Browse Source

Fix lint error/warning while building android template

volzhs 6 years ago
parent
commit
b385a4b053
83 changed files with 6472 additions and 7291 deletions
  1. 10 12
      platform/android/AndroidManifest.xml.template
  2. 1 1
      platform/android/SCsub
  3. 4 3
      platform/android/build.gradle.template
  4. 1 1
      platform/android/export/export.cpp
  5. 47 0
      platform/android/java/README.md
  6. BIN
      platform/android/java/gradle/wrapper/gradle-wrapper.jar
  7. 1 2
      platform/android/java/gradle/wrapper/gradle-wrapper.properties
  8. 42 30
      platform/android/java/gradlew
  9. 84 90
      platform/android/java/gradlew.bat
  10. BIN
      platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png
  11. BIN
      platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png
  12. 0 0
      platform/android/java/res/drawable-nodpi/icon.png
  13. BIN
      platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png
  14. BIN
      platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png
  15. 23 24
      platform/android/java/res/layout/downloading_expansion.xml
  16. 15 11
      platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml
  17. 1 1
      platform/android/java/res/values-ko/strings.xml
  18. 0 6
      platform/android/java/res/values-v11/styles.xml
  19. 0 5
      platform/android/java/res/values-v9/styles.xml
  20. 1 1
      platform/android/java/res/values/strings.xml
  21. 0 110
      platform/android/java/src/com/android/vending/licensing/AESObfuscator.java
  22. 0 397
      platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java
  23. 0 115
      platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java
  24. 0 115
      platform/android/java/src/com/android/vending/licensing/ILicensingService.java
  25. 0 351
      platform/android/java/src/com/android/vending/licensing/LicenseChecker.java
  26. 0 224
      platform/android/java/src/com/android/vending/licensing/LicenseValidator.java
  27. 0 77
      platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java
  28. 0 79
      platform/android/java/src/com/android/vending/licensing/ResponseData.java
  29. 0 276
      platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java
  30. 0 570
      platform/android/java/src/com/android/vending/licensing/util/Base64.java
  31. 85 92
      platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java
  32. 39 41
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java
  33. 165 152
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java
  34. 141 129
      platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java
  35. 265 221
      platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java
  36. 31 31
      platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java
  37. 16 16
      platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java
  38. 3 3
      platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java
  39. 86 83
      platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java
  40. 0 536
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java
  41. 65 66
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java
  42. 0 30
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java
  43. 56 56
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java
  44. 173 179
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java
  45. 700 830
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java
  46. 960 981
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java
  47. 423 464
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java
  48. 143 152
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java
  49. 0 101
      platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java
  50. 110 0
      platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java
  51. 413 0
      platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java
  52. 2 2
      platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java
  53. 0 0
      platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl
  54. 100 0
      platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java
  55. 0 0
      platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl
  56. 100 0
      platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java
  57. 387 0
      platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java
  58. 15 15
      platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java
  59. 232 0
      platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java
  60. 3 3
      platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java
  61. 8 8
      platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java
  62. 18 12
      platform/android/java/src/com/google/android/vending/licensing/Policy.java
  63. 78 0
      platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java
  64. 80 0
      platform/android/java/src/com/google/android/vending/licensing/ResponseData.java
  65. 299 0
      platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java
  66. 51 15
      platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java
  67. 7 7
      platform/android/java/src/com/google/android/vending/licensing/ValidationException.java
  68. 556 0
      platform/android/java/src/com/google/android/vending/licensing/util/Base64.java
  69. 7 7
      platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java
  70. 60 0
      platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java
  71. 67 81
      platform/android/java/src/org/godotengine/godot/Godot.java
  72. 19 19
      platform/android/java/src/org/godotengine/godot/GodotIO.java
  73. 34 25
      platform/android/java/src/org/godotengine/godot/GodotView.java
  74. 43 29
      platform/android/java/src/org/godotengine/godot/input/GodotEditText.java
  75. 1 6
      platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java
  76. 0 209
      platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java
  77. 55 33
      platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java
  78. 0 79
      platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java
  79. 2 2
      platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java
  80. 2 2
      platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java
  81. 58 24
      platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java
  82. 82 47
      platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java
  83. 2 2
      platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java

+ 10 - 12
platform/android/AndroidManifest.xml.template

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-	  package="com.godot.game"
+      xmlns:tools="http://schemas.android.com/tools"
+      package="com.godot.game"
       android:versionCode="1"
       android:versionName="1.0"
       android:installLocation="auto"
@@ -10,32 +11,29 @@
                       android:largeScreens="true"
                       android:xlargeScreens="true"/>
 
-    <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" $$ADD_APPATTRIBUTE_CHUNKS$$ >
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+$$ADD_PERMISSION_CHUNKS$$
+
+    <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" $$ADD_APPATTRIBUTE_CHUNKS$$ >
         <activity android:name="org.godotengine.godot.Godot"
                   android:label="@string/godot_project_name_string"
                   android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
                   android:launchMode="singleTask"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize"
-                  android:resizeableActivity="false">
+                  android:resizeableActivity="false"
+                  tools:ignore="UnusedAttribute">
 
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-	<service android:name="org.godotengine.godot.GodotDownloaderService" />
-
-
-
+    <service android:name="org.godotengine.godot.GodotDownloaderService" />
 
 $$ADD_APPLICATION_CHUNKS$$
 
     </application>
-    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
-
-$$ADD_PERMISSION_CHUNKS$$
-
-<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="27"/>
 
 </manifest>

+ 1 - 1
platform/android/SCsub

@@ -108,7 +108,7 @@ for x in env.android_asset_dirs:
 gradle_default_config_text = ""
 
 minSdk = 18
-targetSdk = 27
+targetSdk = 28
 
 for x in env.android_default_config:
     if x.startswith("minSdkVersion") and int(x.split(" ")[-1]) < minSdk:

+ 4 - 3
platform/android/build.gradle.template

@@ -5,7 +5,7 @@ buildscript {
 		$$GRADLE_REPOSITORY_URLS$$
 	}
 	dependencies {
-		classpath 'com.android.tools.build:gradle:3.2.0'
+		classpath 'com.android.tools.build:gradle:3.2.1'
 		$$GRADLE_CLASSPATH$$
 	}
 }
@@ -22,6 +22,7 @@ allprojects {
 }
 
 dependencies {
+	implementation "com.android.support:support-core-utils:28.0.0"
 	$$GRADLE_DEPENDENCIES$$
 }
 
@@ -29,10 +30,10 @@ android {
 
 	lintOptions {
 		abortOnError false
-		disable 'MissingTranslation'
+		disable 'MissingTranslation','UnusedResources'
 	}
 
-	compileSdkVersion 27
+	compileSdkVersion 28
 	buildToolsVersion "28.0.3"
 	useLibrary 'org.apache.http.legacy'
 

+ 1 - 1
platform/android/export/export.cpp

@@ -1564,7 +1564,7 @@ public:
 				_fix_resources(p_preset, data);
 			}
 
-			if (file == "res/drawable/icon.png") {
+			if (file == "res/drawable-nodpi-v4/icon.png") {
 				bool found = false;
 				for (unsigned int i = 0; i < sizeof(launcher_icons) / sizeof(launcher_icons[0]); ++i) {
 					String icon_path = String(p_preset->get(launcher_icons[i].option_id)).strip_edges();

+ 47 - 0
platform/android/java/README.md

@@ -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();
+```

BIN
platform/android/java/gradle/wrapper/gradle-wrapper.jar


+ 1 - 2
platform/android/java/gradle/wrapper/gradle-wrapper.properties

@@ -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

+ 42 - 30
platform/android/java/gradlew

@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
 
 ##############################################################################
 ##
@@ -6,20 +6,38 @@
 ##
 ##############################################################################
 
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+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 ( ) {
+warn () {
     echo "$*"
 }
 
-die ( ) {
+die () {
     echo
     echo "$*"
     echo
@@ -30,6 +48,7 @@ die ( ) {
 cygwin=false
 msys=false
 darwin=false
+nonstop=false
 case "`uname`" in
   CYGWIN* )
     cygwin=true
@@ -40,26 +59,11 @@ case "`uname`" in
   MINGW* )
     msys=true
     ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
 esac
 
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
 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 "$@")
+
+# 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" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+exec "$JAVACMD" "$@"

+ 84 - 90
platform/android/java/gradlew.bat

@@ -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

BIN
platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png


BIN
platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png


+ 0 - 0
platform/android/java/res/drawable/icon.png → platform/android/java/res/drawable-nodpi/icon.png


BIN
platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png


BIN
platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png


+ 23 - 24
platform/android/java/res/layout/downloading_expansion.xml

@@ -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>
 
@@ -86,33 +81,35 @@
                 android:orientation="horizontal" >
 
                 <Button
-                    android:id="@+id/pauseButton"
+                    android:id="@+id/cancelButton"
                     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_marginLeft="5dp"
                     android:layout_marginRight="5dp"
                     android:layout_marginTop="10dp"
                     android:layout_weight="0"
                     android:minHeight="40dp"
                     android:minWidth="94dp"
-                    android:text="@string/text_button_pause" />
+                    android:text="@string/text_button_cancel"
+                    android:visibility="gone"
+                    style="?android:attr/buttonBarButtonStyle" />
 
                 <Button
-                    android:id="@+id/cancelButton"
+                    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="5dp"
-                    android:layout_marginRight="5dp"
+                    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_cancel"
-                    android:visibility="gone" />
+                    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>
 

+ 15 - 11
platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml

@@ -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>
 

+ 1 - 1
platform/android/java/res/values-ko/strings.xml

@@ -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>

+ 0 - 6
platform/android/java/res/values-v11/styles.xml

@@ -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>

+ 0 - 5
platform/android/java/res/values-v9/styles.xml

@@ -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>

+ 1 - 1
platform/android/java/res/values/strings.xml

@@ -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>

+ 0 - 110
platform/android/java/src/com/android/vending/licensing/AESObfuscator.java

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

+ 0 - 397
platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java

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

+ 0 - 115
platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java

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

+ 0 - 115
platform/android/java/src/com/android/vending/licensing/ILicensingService.java

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

+ 0 - 351
platform/android/java/src/com/android/vending/licensing/LicenseChecker.java

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

+ 0 - 224
platform/android/java/src/com/android/vending/licensing/LicenseValidator.java

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

+ 0 - 77
platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java

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

+ 0 - 79
platform/android/java/src/com/android/vending/licensing/ResponseData.java

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

+ 0 - 276
platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java

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

+ 0 - 570
platform/android/java/src/com/android/vending/licensing/util/Base64.java

@@ -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 [email protected]
- * @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;
-  }
-}

+ 85 - 92
platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java

@@ -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 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 a successful download */
-    public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
+	/** 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 clicking an incomplete/failed download  */
-    public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
+	/** 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";
+	/** 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 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 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;
 }

+ 39 - 41
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java

@@ -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
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
+	public long mOverallTotal;
+	public long mOverallProgress;
+	public long mTimeRemaining; // time remaining
+	public float mCurrentSpeed; // speed in KB/S
 
-    @Override
-    public void writeToParcel(Parcel p, int i) {
-        p.writeLong(mOverallTotal);
-        p.writeLong(mOverallProgress);
-        p.writeLong(mTimeRemaining);
-        p.writeFloat(mCurrentSpeed);
-    }
+	@Override
+	public int describeContents() {
+		return 0;
+	}
 
-    public DownloadProgressInfo(Parcel p) {
-        mOverallTotal = p.readLong();
-        mOverallProgress = p.readLong();
-        mTimeRemaining = p.readLong();
-        mCurrentSpeed = p.readFloat();
-    }
+	@Override
+	public void writeToParcel(Parcel p, int i) {
+		p.writeLong(mOverallTotal);
+		p.writeLong(mOverallProgress);
+		p.writeLong(mTimeRemaining);
+		p.writeFloat(mCurrentSpeed);
+	}
 
-    public DownloadProgressInfo(long overallTotal, long overallProgress,
-            long timeRemaining,
-            float currentSpeed) {
-        this.mOverallTotal = overallTotal;
-        this.mOverallProgress = overallProgress;
-        this.mTimeRemaining = timeRemaining;
-        this.mCurrentSpeed = currentSpeed;
-    }
+	public DownloadProgressInfo(Parcel p) {
+		mOverallTotal = p.readLong();
+		mOverallProgress = p.readLong();
+		mTimeRemaining = p.readLong();
+		mCurrentSpeed = p.readFloat();
+	}
 
-    public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
-        @Override
-        public DownloadProgressInfo createFromParcel(Parcel parcel) {
-            return new DownloadProgressInfo(parcel);
-        }
+	public DownloadProgressInfo(long overallTotal, long overallProgress,
+			long timeRemaining,
+			float currentSpeed) {
+		this.mOverallTotal = overallTotal;
+		this.mOverallProgress = overallProgress;
+		this.mTimeRemaining = timeRemaining;
+		this.mCurrentSpeed = currentSpeed;
+	}
 
-        @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];
+		}
+	};
 }

+ 165 - 152
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java

@@ -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();
+			}
+		}
 
-        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;
-        }
+		public Proxy(Messenger msg) {
+			mServiceMessenger = msg;
+		}
 
-        @Override
-        public void onServiceConnected(Messenger m) {
-            /**
+		@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);
+
+		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);
+				}
+			}
+		}
 
-        public Stub(IDownloaderClient itf, Class<?> downloaderService) {
-            mItf = itf;
-            mDownloaderServiceClass = downloaderService;
-        }
+		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);
+	}
 }

+ 141 - 129
platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java

@@ -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 String PARAMS_FLAGS = "flags";
-    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
-
-    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();
-            }
-        }
-
-        public Proxy(Messenger msg) {
-            mMsg = msg;
-        }
-
-        @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 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 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);
-        }
-    }
-
-    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;
-                }
-            }
-        });
-
-        public Stub(IDownloaderService itf) {
-            mItf = itf;
-        }
-
-        @Override
-        public Messenger getMessenger() {
-            return mMessenger;
-        }
-
-        @Override
-        public void connect(Context c) {
-
-        }
-
-        @Override
-        public void disconnect(Context c) {
-
-        }
-    }
-
-    /**
+	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;
+
+	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();
+			}
+		}
+
+		public Proxy(Messenger msg) {
+			mMsg = msg;
+		}
+
+		@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 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 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);
+		}
+	}
+
+	private static class Stub implements IStub {
+		private IDownloaderService mItf = null;
+		private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
+		final Messenger mMessenger = new Messenger(mMsgHandler);
+
+		private static class MessengerHandlerServer extends Handler {
+			private final WeakReference<Stub> mDownloader;
+			public MessengerHandlerServer(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_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 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);
+	}
 }

+ 265 - 221
platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java

@@ -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 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;
+	}
+
+	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 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 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.
+	/**
+     * 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;
+		}
+	}
 }

+ 31 - 31
platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java

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

+ 16 - 16
platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java

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

+ 3 - 3
platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java

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

+ 86 - 83
platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java

@@ -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;
-
-    public SystemFacade(Context context) {
-        mContext = context;
-        mNotificationManager = (NotificationManager)
-                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-    }
-
-    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;
-        }
-
-        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;
-        }
-
-        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 getRecommendedMaxBytesOverMobile() {
-        return 2097152L;
-    }
-
-    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 void postNotification(long id, Notification notification) {
-        /**
+	private Context mContext;
+	private NotificationManager mNotificationManager;
+
+	public SystemFacade(Context context) {
+		mContext = context;
+		mNotificationManager = (NotificationManager)
+									   mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+	}
+
+	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;
+		}
+
+		@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;
+		}
+
+		@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 getRecommendedMaxBytesOverMobile() {
+		return 2097152L;
+	}
+
+	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 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();
+	}
 }

+ 0 - 536
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java

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

+ 65 - 66
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java

@@ -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");
+			}
+		}
+	}
 }

+ 0 - 30
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java

@@ -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();
-    }
-}

+ 56 - 56
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java

@@ -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);
+	}
 }

+ 173 - 179
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java

@@ -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 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;
-
-    static final String LOGTAG = "DownloadNotification";
-    static final int NOTIFICATION_ID = LOGTAG.hashCode();
-
-    public PendingIntent getClientIntent() {
-        return mContentIntent;
-    }
-
-    public void setClientIntent(PendingIntent mClientIntent) {
-        this.mContentIntent = mClientIntent;
-    }
-
-    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;
-
-            // 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_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_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());
-        }
-    }
-
-    @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());
-    }
-
-    public interface ICustomNotification {
-        void setTitle(CharSequence title);
-
-        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);
-    }
-
-    /**
+	private int mState;
+	private final Context mContext;
+	private final NotificationManager mNotificationManager;
+	private CharSequence mCurrentTitle;
+
+	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();
+
+	public PendingIntent getClientIntent() {
+		return mContentIntent;
+	}
+
+	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);
+		}
+	}
+
+	@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;
+
+				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_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;
+
+				default:
+					iconResource = android.R.drawable.stat_sys_warning;
+					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
+					ongoingEvent = true;
+					break;
+			}
+
+			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());
+		}
+	}
+
+	@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());
+	}
+
+	/**
      * 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;
-
-    }
-
-    @Override
-    public void onServiceConnected(Messenger m) {
-    }
-
+	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);
+
+		mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
+		mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
+
+		mCurrentBuilder = mBuilder;
+	}
+
+	@Override
+	public void onServiceConnected(Messenger m) {
+	}
 }

+ 700 - 830
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2015 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.
@@ -20,14 +20,7 @@ import com.google.android.vending.expansion.downloader.Constants;
 import com.google.android.vending.expansion.downloader.Helpers;
 import com.google.android.vending.expansion.downloader.IDownloaderClient;
 
-import org.apache.http.Header;
-import org.apache.http.HttpHost;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.conn.params.ConnRouteParams;
-
 import android.content.Context;
-import android.net.Proxy;
 import android.os.PowerManager;
 import android.os.Process;
 import android.util.Log;
@@ -38,8 +31,8 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.SyncFailedException;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.Locale;
 
 /**
@@ -47,917 +40,794 @@ import java.util.Locale;
  */
 public class DownloadThread {
 
-    private Context mContext;
-    private DownloadInfo mInfo;
-    private DownloaderService mService;
-    private final DownloadsDB mDB;
-    private final DownloadNotification mNotification;
-    private String mUserAgent;
-
-    public DownloadThread(DownloadInfo info, DownloaderService service,
-            DownloadNotification notification) {
-        mContext = service;
-        mInfo = info;
-        mService = service;
-        mNotification = notification;
-        mDB = DownloadsDB.getDB(service);
-        mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";"
-                + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/"
-                + android.os.Build.ID + ")" +
-                service.getPackageName();
-    }
-
-    /**
+	private Context mContext;
+	private DownloadInfo mInfo;
+	private DownloaderService mService;
+	private final DownloadsDB mDB;
+	private final DownloadNotification mNotification;
+	private String mUserAgent;
+
+	public DownloadThread(DownloadInfo info, DownloaderService service,
+			DownloadNotification notification) {
+		mContext = service;
+		mInfo = info;
+		mService = service;
+		mNotification = notification;
+		mDB = DownloadsDB.getDB(service);
+		mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" + android.os.Build.ID + ")" +
+					 service.getPackageName();
+	}
+
+	/**
      * Returns the default user agent
      */
-    private String userAgent() {
-        return mUserAgent;
-    }
+	private String userAgent() {
+		return mUserAgent;
+	}
 
-    /**
+	/**
      * State for the entire run() method.
      */
-    private static class State {
-        public String mFilename;
-        public FileOutputStream mStream;
-        public boolean mCountRetry = false;
-        public int mRetryAfter = 0;
-        public int mRedirectCount = 0;
-        public String mNewUri;
-        public boolean mGotData = false;
-        public String mRequestUri;
-
-        public State(DownloadInfo info, DownloaderService service) {
-            mRedirectCount = info.mRedirectCount;
-            mRequestUri = info.mUri;
-            mFilename = service.generateTempSaveFileName(info.mFileName);
-        }
-    }
-
-    /**
+	private static class State {
+		public String mFilename;
+		public FileOutputStream mStream;
+		public boolean mCountRetry = false;
+		public int mRetryAfter = 0;
+		public int mRedirectCount = 0;
+		public String mNewUri;
+		public boolean mGotData = false;
+		public String mRequestUri;
+
+		public State(DownloadInfo info, DownloaderService service) {
+			mRedirectCount = info.mRedirectCount;
+			mRequestUri = info.mUri;
+			mFilename = service.generateTempSaveFileName(info.mFileName);
+		}
+	}
+
+	/**
      * State within executeDownload()
      */
-    private static class InnerState {
-        public int mBytesSoFar = 0;
-        public int mBytesThisSession = 0;
-        public String mHeaderETag;
-        public boolean mContinuingDownload = false;
-        public String mHeaderContentLength;
-        public String mHeaderContentDisposition;
-        public String mHeaderContentLocation;
-        public int mBytesNotified = 0;
-        public long mTimeLastNotification = 0;
-    }
-
-    /**
+	private static class InnerState {
+		public int mBytesSoFar = 0;
+		public int mBytesThisSession = 0;
+		public String mHeaderETag;
+		public boolean mContinuingDownload = false;
+		public String mHeaderContentLength;
+		public String mHeaderContentDisposition;
+		public String mHeaderContentLocation;
+		public int mBytesNotified = 0;
+		public long mTimeLastNotification = 0;
+	}
+
+	/**
      * Raised from methods called by run() to indicate that the current request
      * should be stopped immediately. Note the message passed to this exception
      * will be logged and therefore must be guaranteed not to contain any PII,
      * meaning it generally can't include any information about the request URI,
      * headers, or destination filename.
      */
-    private class StopRequest extends Throwable {
-        /**
-		 * 
-		 */
-        private static final long serialVersionUID = 6338592678988347973L;
-        public int mFinalStatus;
-
-        public StopRequest(int finalStatus, String message) {
-            super(message);
-            mFinalStatus = finalStatus;
-        }
-
-        public StopRequest(int finalStatus, String message, Throwable throwable) {
-            super(message, throwable);
-            mFinalStatus = finalStatus;
-        }
-    }
-
-    /**
+	private class StopRequest extends Throwable {
+
+		private static final long serialVersionUID = 6338592678988347973L;
+		public int mFinalStatus;
+
+		public StopRequest(int finalStatus, String message) {
+			super(message);
+			mFinalStatus = finalStatus;
+		}
+
+		public StopRequest(int finalStatus, String message, Throwable throwable) {
+			super(message, throwable);
+			mFinalStatus = finalStatus;
+		}
+	}
+
+	/**
      * Raised from methods called by executeDownload() to indicate that the
      * download should be retried immediately.
      */
-    private class RetryDownload extends Throwable {
-
-        /**
-		 * 
-		 */
-        private static final long serialVersionUID = 6196036036517540229L;
-    }
-
-    /**
-     * Returns the preferred proxy to be used by clients. This is a wrapper
-     * around {@link android.net.Proxy#getHost()}. Currently no proxy will be
-     * returned for localhost or if the active network is Wi-Fi.
-     * 
-     * @param context the context which will be passed to
-     *            {@link android.net.Proxy#getHost()}
-     * @param url the target URL for the request
-     * @note Calling this method requires permission
-     *       android.permission.ACCESS_NETWORK_STATE
-     * @return The preferred proxy to be used by clients, or null if there is no
-     *         proxy.
-     */
-    public HttpHost getPreferredHttpHost(Context context,
-            String url) {
-        if (!isLocalHost(url) && !mService.isWiFi()) {
-            final String proxyHost = Proxy.getHost(context);
-            if (proxyHost != null) {
-                return new HttpHost(proxyHost, Proxy.getPort(context), "http");
-            }
-        }
-
-        return null;
-    }
-
-    static final private boolean isLocalHost(String url) {
-        if (url == null) {
-            return false;
-        }
-
-        try {
-            final URI uri = URI.create(url);
-            final String host = uri.getHost();
-            if (host != null) {
-                // TODO: InetAddress.isLoopbackAddress should be used to check
-                // for localhost. However no public factory methods exist which
-                // can be used without triggering DNS lookup if host is not
-                // localhost.
-                if (host.equalsIgnoreCase("localhost") ||
-                        host.equals("127.0.0.1") ||
-                        host.equals("[::1]")) {
-                    return true;
-                }
-            }
-        } catch (IllegalArgumentException iex) {
-            // Ignore (URI.create)
-        }
-
-        return false;
-    }
-
-    /**
+	private class RetryDownload extends Throwable {
+
+		private static final long serialVersionUID = 6196036036517540229L;
+	}
+
+	/**
      * Executes the download in a separate thread
      */
-    public void run() {
-        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
-        State state = new State(mInfo, mService);
-        AndroidHttpClient client = null;
-        PowerManager.WakeLock wakeLock = null;
-        int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
-
-        try {
-            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
-            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
-            wakeLock.acquire();
-
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
-                Log.v(Constants.TAG, "  at " + mInfo.mUri);
-            }
-
-            client = AndroidHttpClient.newInstance(userAgent(), mContext);
-
-            boolean finished = false;
-            while (!finished) {
-                if (Constants.LOGV) {
-                    Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
-                    Log.v(Constants.TAG, "  at " + mInfo.mUri);
-                }
-                // Set or unset proxy, which may have changed since last GET
-                // request.
-                // setDefaultProxy() supports null as proxy parameter.
-                ConnRouteParams.setDefaultProxy(client.getParams(),
-                        getPreferredHttpHost(mContext, state.mRequestUri));
-                HttpGet request = new HttpGet(state.mRequestUri);
-                try {
-                    executeDownload(state, client, request);
-                    finished = true;
-                } catch (RetryDownload exc) {
-                    // fall through
-                } finally {
-                    request.abort();
-                    request = null;
-                }
-            }
-
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
-                Log.v(Constants.TAG, "  at " + mInfo.mUri);
-            }
-            finalizeDestinationFile(state);
-            finalStatus = DownloaderService.STATUS_SUCCESS;
-        } catch (StopRequest error) {
-            // remove the cause before printing, in case it contains PII
-            Log.w(Constants.TAG,
-                    "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
-            error.printStackTrace();
-            finalStatus = error.mFinalStatus;
-            // fall through to finally block
-        } catch (Throwable ex) { // sometimes the socket code throws unchecked
-                                 // exceptions
-            Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
-            finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
-            // falls through to the code that reports an error
-        } finally {
-            if (wakeLock != null) {
-                wakeLock.release();
-                wakeLock = null;
-            }
-            if (client != null) {
-                client.close();
-                client = null;
-            }
-            cleanupDestination(state, finalStatus);
-            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
-                    state.mRedirectCount, state.mGotData, state.mFilename);
-        }
-    }
-
-    /**
+	public void run() {
+		Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+		State state = new State(mInfo, mService);
+		PowerManager.WakeLock wakeLock = null;
+		int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+
+		try {
+			PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
+			wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
+			wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
+
+			if (Constants.LOGV) {
+				Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+				Log.v(Constants.TAG, "  at " + mInfo.mUri);
+			}
+
+			boolean finished = false;
+			while (!finished) {
+				if (Constants.LOGV) {
+					Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
+					Log.v(Constants.TAG, "  at " + mInfo.mUri);
+				}
+				// Set or unset proxy, which may have changed since last GET
+				// request.
+				// setDefaultProxy() supports null as proxy parameter.
+				URL url = new URL(state.mRequestUri);
+				HttpURLConnection request = (HttpURLConnection)url.openConnection();
+				request.setRequestProperty("User-Agent", userAgent());
+				try {
+					executeDownload(state, request);
+					finished = true;
+				} catch (RetryDownload exc) {
+					// fall through
+				} finally {
+					request.disconnect();
+					request = null;
+				}
+			}
+
+			if (Constants.LOGV) {
+				Log.v(Constants.TAG, "download completed for " + mInfo.mFileName);
+				Log.v(Constants.TAG, "  at " + mInfo.mUri);
+			}
+			finalizeDestinationFile(state);
+			finalStatus = DownloaderService.STATUS_SUCCESS;
+		} catch (StopRequest error) {
+			// remove the cause before printing, in case it contains PII
+			Log.w(Constants.TAG,
+					"Aborting request for download " + mInfo.mFileName + ": " + error.getMessage());
+			error.printStackTrace();
+			finalStatus = error.mFinalStatus;
+			// fall through to finally block
+		} catch (Throwable ex) { // sometimes the socket code throws unchecked
+			// exceptions
+			Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex);
+			finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
+			// falls through to the code that reports an error
+		} finally {
+			if (wakeLock != null) {
+				wakeLock.release();
+				wakeLock = null;
+			}
+			cleanupDestination(state, finalStatus);
+			notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
+					state.mRedirectCount, state.mGotData, state.mFilename);
+		}
+	}
+
+	/**
      * Fully execute a single download request - setup and send the request,
      * handle the response, and transfer the data to the destination file.
      */
-    private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
-            throws StopRequest, RetryDownload {
-        InnerState innerState = new InnerState();
-        byte data[] = new byte[Constants.BUFFER_SIZE];
+	private void executeDownload(State state, HttpURLConnection request)
+			throws StopRequest, RetryDownload {
+		InnerState innerState = new InnerState();
+		byte data[] = new byte[Constants.BUFFER_SIZE];
 
-        checkPausedOrCanceled(state);
+		checkPausedOrCanceled(state);
 
-        setupDestinationFile(state, innerState);
-        addRequestHeaders(innerState, request);
+		setupDestinationFile(state, innerState);
+		addRequestHeaders(innerState, request);
 
-        // check just before sending the request to avoid using an invalid
-        // connection at all
-        checkConnectivity(state);
+		// check just before sending the request to avoid using an invalid
+		// connection at all
+		checkConnectivity(state);
 
-        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
-        HttpResponse response = sendRequest(state, client, request);
-        handleExceptionalStatus(state, innerState, response);
+		mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING);
+		int responseCode = sendRequest(state, request);
+		handleExceptionalStatus(state, innerState, request, responseCode);
 
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
-        }
+		if (Constants.LOGV) {
+			Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+		}
 
-        processResponseHeaders(state, innerState, response);
-        InputStream entityStream = openResponseEntity(state, response);
-        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
-        transferData(state, innerState, data, entityStream);
-    }
+		processResponseHeaders(state, innerState, request);
+		InputStream entityStream = openResponseEntity(state, request);
+		mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING);
+		transferData(state, innerState, data, entityStream);
+	}
 
-    /**
+	/**
      * Check if current connectivity is valid for this request.
      */
-    private void checkConnectivity(State state) throws StopRequest {
-        switch (mService.getNetworkAvailabilityState(mDB)) {
-            case DownloaderService.NETWORK_OK:
-                return;
-            case DownloaderService.NETWORK_NO_CONNECTION:
-                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
-                        "waiting for network to return");
-            case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
-                throw new StopRequest(
-                        DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
-                        "waiting for wifi or for download over cellular to be authorized");
-            case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
-                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
-                        "roaming is not allowed");
-            case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
-                throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
-        }
-    }
-
-    /**
+	private void checkConnectivity(State state) throws StopRequest {
+		switch (mService.getNetworkAvailabilityState(mDB)) {
+			case DownloaderService.NETWORK_OK:
+				return;
+			case DownloaderService.NETWORK_NO_CONNECTION:
+				throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+						"waiting for network to return");
+			case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
+				throw new StopRequest(
+						DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION,
+						"waiting for wifi or for download over cellular to be authorized");
+			case DownloaderService.NETWORK_CANNOT_USE_ROAMING:
+				throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK,
+						"roaming is not allowed");
+			case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE:
+				throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi");
+		}
+	}
+
+	/**
      * Transfer as much data as possible from the HTTP response to the
      * destination file.
-     * 
+     *
      * @param data buffer to use to read data
      * @param entityStream stream for reading the HTTP response entity
      */
-    private void transferData(State state, InnerState innerState, byte[] data,
-            InputStream entityStream) throws StopRequest {
-        for (;;) {
-            int bytesRead = readFromResponse(state, innerState, data, entityStream);
-            if (bytesRead == -1) { // success, end of stream already reached
-                handleEndOfStream(state, innerState);
-                return;
-            }
-
-            state.mGotData = true;
-            writeDataToDestination(state, data, bytesRead);
-            innerState.mBytesSoFar += bytesRead;
-            innerState.mBytesThisSession += bytesRead;
-            reportProgress(state, innerState);
-
-            checkPausedOrCanceled(state);
-        }
-    }
-
-    /**
+	private void transferData(State state, InnerState innerState, byte[] data,
+			InputStream entityStream) throws StopRequest {
+		for (;;) {
+			int bytesRead = readFromResponse(state, innerState, data, entityStream);
+			if (bytesRead == -1) { // success, end of stream already reached
+				handleEndOfStream(state, innerState);
+				return;
+			}
+
+			state.mGotData = true;
+			writeDataToDestination(state, data, bytesRead);
+			innerState.mBytesSoFar += bytesRead;
+			innerState.mBytesThisSession += bytesRead;
+			reportProgress(state, innerState);
+
+			checkPausedOrCanceled(state);
+		}
+	}
+
+	/**
      * Called after a successful completion to take any necessary action on the
      * downloaded file.
      */
-    private void finalizeDestinationFile(State state) throws StopRequest {
-        syncDestination(state);
-        String tempFilename = state.mFilename;
-        String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
-        if (!state.mFilename.equals(finalFilename)) {
-            File startFile = new File(tempFilename);
-            File destFile = new File(finalFilename);
-            if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
-                if (!startFile.renameTo(destFile)) {
-                    throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-                            "unable to finalize destination file");
-                }
-            } else {
-                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
-                        "file delivered with incorrect size. probably due to network not browser configured");
-            }
-        }
-    }
-
-    /**
+	private void finalizeDestinationFile(State state) throws StopRequest {
+		syncDestination(state);
+		String tempFilename = state.mFilename;
+		String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName);
+		if (!state.mFilename.equals(finalFilename)) {
+			File startFile = new File(tempFilename);
+			File destFile = new File(finalFilename);
+			if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) {
+				if (!startFile.renameTo(destFile)) {
+					throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+							"unable to finalize destination file");
+				}
+			} else {
+				throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+						"file delivered with incorrect size. probably due to network not browser configured");
+			}
+		}
+	}
+
+	/**
      * Called just before the thread finishes, regardless of status, to take any
      * necessary action on the downloaded file.
      */
-    private void cleanupDestination(State state, int finalStatus) {
-        closeDestination(state);
-        if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
-            new File(state.mFilename).delete();
-            state.mFilename = null;
-        }
-    }
-
-    /**
+	private void cleanupDestination(State state, int finalStatus) {
+		closeDestination(state);
+		if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) {
+			new File(state.mFilename).delete();
+			state.mFilename = null;
+		}
+	}
+
+	/**
      * Sync the destination file to storage.
      */
-    private void syncDestination(State state) {
-        FileOutputStream downloadedFileStream = null;
-        try {
-            downloadedFileStream = new FileOutputStream(state.mFilename, true);
-            downloadedFileStream.getFD().sync();
-        } catch (FileNotFoundException ex) {
-            Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
-        } catch (SyncFailedException ex) {
-            Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
-        } catch (IOException ex) {
-            Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
-        } catch (RuntimeException ex) {
-            Log.w(Constants.TAG, "exception while syncing file: ", ex);
-        } finally {
-            if (downloadedFileStream != null) {
-                try {
-                    downloadedFileStream.close();
-                } catch (IOException ex) {
-                    Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
-                } catch (RuntimeException ex) {
-                    Log.w(Constants.TAG, "exception while closing file: ", ex);
-                }
-            }
-        }
-    }
-
-    /**
+	private void syncDestination(State state) {
+		FileOutputStream downloadedFileStream = null;
+		try {
+			downloadedFileStream = new FileOutputStream(state.mFilename, true);
+			downloadedFileStream.getFD().sync();
+		} catch (FileNotFoundException ex) {
+			Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
+		} catch (SyncFailedException ex) {
+			Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
+		} catch (IOException ex) {
+			Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
+		} catch (RuntimeException ex) {
+			Log.w(Constants.TAG, "exception while syncing file: ", ex);
+		} finally {
+			if (downloadedFileStream != null) {
+				try {
+					downloadedFileStream.close();
+				} catch (IOException ex) {
+					Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
+				} catch (RuntimeException ex) {
+					Log.w(Constants.TAG, "exception while closing file: ", ex);
+				}
+			}
+		}
+	}
+
+	/**
      * Close the destination output stream.
      */
-    private void closeDestination(State state) {
-        try {
-            // close the file
-            if (state.mStream != null) {
-                state.mStream.close();
-                state.mStream = null;
-            }
-        } catch (IOException ex) {
-            if (Constants.LOGV) {
-                Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
-            }
-            // nothing can really be done if the file can't be closed
-        }
-    }
-
-    /**
+	private void closeDestination(State state) {
+		try {
+			// close the file
+			if (state.mStream != null) {
+				state.mStream.close();
+				state.mStream = null;
+			}
+		} catch (IOException ex) {
+			if (Constants.LOGV) {
+				Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+			}
+			// nothing can really be done if the file can't be closed
+		}
+	}
+
+	/**
      * Check if the download has been paused or canceled, stopping the request
      * appropriately if it has been.
      */
-    private void checkPausedOrCanceled(State state) throws StopRequest {
-        if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
-            int status = mService.getStatus();
-            switch (status) {
-                case DownloaderService.STATUS_PAUSED_BY_APP:
-                    throw new StopRequest(mService.getStatus(),
-                            "download paused");
-            }
-        }
-    }
-
-    /**
+	private void checkPausedOrCanceled(State state) throws StopRequest {
+		if (mService.getControl() == DownloaderService.CONTROL_PAUSED) {
+			int status = mService.getStatus();
+			switch (status) {
+				case DownloaderService.STATUS_PAUSED_BY_APP:
+					throw new StopRequest(mService.getStatus(),
+							"download paused");
+			}
+		}
+	}
+
+	/**
      * Report download progress through the database if necessary.
      */
-    private void reportProgress(State state, InnerState innerState) {
-        long now = System.currentTimeMillis();
-        if (innerState.mBytesSoFar - innerState.mBytesNotified
-                > Constants.MIN_PROGRESS_STEP
-                && now - innerState.mTimeLastNotification
-                > Constants.MIN_PROGRESS_TIME) {
-            // we store progress updates to the database here
-            mInfo.mCurrentBytes = innerState.mBytesSoFar;
-            mDB.updateDownloadCurrentBytes(mInfo);
-
-            innerState.mBytesNotified = innerState.mBytesSoFar;
-            innerState.mTimeLastNotification = now;
-
-            long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
-
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of "
-                        + mInfo.mTotalBytes);
-                Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of "
-                        + mService.mTotalLength);
-            }
-
-            mService.notifyUpdateBytes(totalBytesSoFar);
-        }
-    }
-
-    /**
+	private void reportProgress(State state, InnerState innerState) {
+		long now = System.currentTimeMillis();
+		if (innerState.mBytesSoFar - innerState.mBytesNotified > Constants.MIN_PROGRESS_STEP && now - innerState.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
+			// we store progress updates to the database here
+			mInfo.mCurrentBytes = innerState.mBytesSoFar;
+			mDB.updateDownloadCurrentBytes(mInfo);
+
+			innerState.mBytesNotified = innerState.mBytesSoFar;
+			innerState.mTimeLastNotification = now;
+
+			long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar;
+
+			if (Constants.LOGVV) {
+				Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " + mInfo.mTotalBytes);
+				Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of " + mService.mTotalLength);
+			}
+
+			mService.notifyUpdateBytes(totalBytesSoFar);
+		}
+	}
+
+	/**
      * Write a data buffer to the destination file.
-     * 
+     *
      * @param data buffer containing the data to write
      * @param bytesRead how many bytes to write from the buffer
      */
-    private void writeDataToDestination(State state, byte[] data, int bytesRead)
-            throws StopRequest {
-        for (;;) {
-            try {
-                if (state.mStream == null) {
-                    state.mStream = new FileOutputStream(state.mFilename, true);
-                }
-                state.mStream.write(data, 0, bytesRead);
-                // we close after every write --- this may be too inefficient
-                closeDestination(state);
-                return;
-            } catch (IOException ex) {
-                if (!Helpers.isExternalMediaMounted()) {
-                    throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
-                            "external media not mounted while writing destination file");
-                }
-
-                long availableBytes =
-                        Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
-                if (availableBytes < bytesRead) {
-                    throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
-                            "insufficient space while writing destination file", ex);
-                }
-                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-                        "while writing destination file: " + ex.toString(), ex);
-            }
-        }
-    }
-
-    /**
+	private void writeDataToDestination(State state, byte[] data, int bytesRead)
+			throws StopRequest {
+		for (;;) {
+			try {
+				if (state.mStream == null) {
+					state.mStream = new FileOutputStream(state.mFilename, true);
+				}
+				state.mStream.write(data, 0, bytesRead);
+				// we close after every write --- this may be too inefficient
+				closeDestination(state);
+				return;
+			} catch (IOException ex) {
+				if (!Helpers.isExternalMediaMounted()) {
+					throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR,
+							"external media not mounted while writing destination file");
+				}
+
+				long availableBytes =
+						Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
+				if (availableBytes < bytesRead) {
+					throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR,
+							"insufficient space while writing destination file", ex);
+				}
+				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+						"while writing destination file: " + ex.toString(), ex);
+			}
+		}
+	}
+
+	/**
      * Called when we've reached the end of the HTTP response stream, to update
      * the database and check for consistency.
      */
-    private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
-        mInfo.mCurrentBytes = innerState.mBytesSoFar;
-        // this should always be set from the market
-        // if ( innerState.mHeaderContentLength == null ) {
-        // mInfo.mTotalBytes = innerState.mBytesSoFar;
-        // }
-        mDB.updateDownload(mInfo);
-
-        boolean lengthMismatched = (innerState.mHeaderContentLength != null)
-                && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
-        if (lengthMismatched) {
-            if (cannotResume(innerState)) {
-                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-                        "mismatched content length");
-            } else {
-                throw new StopRequest(getFinalStatusForHttpError(state),
-                        "closed socket before end of file");
-            }
-        }
-    }
-
-    private boolean cannotResume(InnerState innerState) {
-        return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
-    }
-
-    /**
+	private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+		mInfo.mCurrentBytes = innerState.mBytesSoFar;
+		// this should always be set from the market
+		// if ( innerState.mHeaderContentLength == null ) {
+		// mInfo.mTotalBytes = innerState.mBytesSoFar;
+		// }
+		mDB.updateDownload(mInfo);
+
+		boolean lengthMismatched = (innerState.mHeaderContentLength != null) && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
+		if (lengthMismatched) {
+			if (cannotResume(innerState)) {
+				throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+						"mismatched content length");
+			} else {
+				throw new StopRequest(getFinalStatusForHttpError(state),
+						"closed socket before end of file");
+			}
+		}
+	}
+
+	private boolean cannotResume(InnerState innerState) {
+		return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null;
+	}
+
+	/**
      * Read some data from the HTTP response stream, handling I/O errors.
-     * 
+     *
      * @param data buffer to use to read data
      * @param entityStream stream for reading the HTTP response entity
      * @return the number of bytes actually read or -1 if the end of the stream
      *         has been reached
      */
-    private int readFromResponse(State state, InnerState innerState, byte[] data,
-            InputStream entityStream) throws StopRequest {
-        try {
-            return entityStream.read(data);
-        } catch (IOException ex) {
-            logNetworkState();
-            mInfo.mCurrentBytes = innerState.mBytesSoFar;
-            mDB.updateDownload(mInfo);
-            if (cannotResume(innerState)) {
-                String message = "while reading response: " + ex.toString()
-                        + ", can't resume interrupted download with no ETag";
-                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-                        message, ex);
-            } else {
-                throw new StopRequest(getFinalStatusForHttpError(state),
-                        "while reading response: " + ex.toString(), ex);
-            }
-        }
-    }
-
-    /**
+	private int readFromResponse(State state, InnerState innerState, byte[] data,
+			InputStream entityStream) throws StopRequest {
+		try {
+			return entityStream.read(data);
+		} catch (IOException ex) {
+			logNetworkState();
+			mInfo.mCurrentBytes = innerState.mBytesSoFar;
+			mDB.updateDownload(mInfo);
+			if (cannotResume(innerState)) {
+				String message = "while reading response: " + ex.toString() + ", can't resume interrupted download with no ETag";
+				throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+						message, ex);
+			} else {
+				throw new StopRequest(getFinalStatusForHttpError(state),
+						"while reading response: " + ex.toString(), ex);
+			}
+		}
+	}
+
+	/**
      * Open a stream for the HTTP response entity, handling I/O errors.
-     * 
+     *
      * @return an InputStream to read the response entity
      */
-    private InputStream openResponseEntity(State state, HttpResponse response)
-            throws StopRequest {
-        try {
-            return response.getEntity().getContent();
-        } catch (IOException ex) {
-            logNetworkState();
-            throw new StopRequest(getFinalStatusForHttpError(state),
-                    "while getting entity: " + ex.toString(), ex);
-        }
-    }
-
-    private void logNetworkState() {
-        if (Constants.LOGX) {
-            Log.i(Constants.TAG,
-                    "Net "
-                            + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up"
-                                    : "Down"));
-        }
-    }
-
-    /**
+	private InputStream openResponseEntity(State state, HttpURLConnection response)
+			throws StopRequest {
+		try {
+			return response.getInputStream();
+		} catch (IOException ex) {
+			logNetworkState();
+			throw new StopRequest(getFinalStatusForHttpError(state),
+					"while getting entity: " + ex.toString(), ex);
+		}
+	}
+
+	private void logNetworkState() {
+		if (Constants.LOGX) {
+			Log.i(Constants.TAG,
+					"Net " + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up" : "Down"));
+		}
+	}
+
+	/**
      * Read HTTP response headers and take appropriate action, including setting
      * up the destination file and updating the database.
      */
-    private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
-            throws StopRequest {
-        if (innerState.mContinuingDownload) {
-            // ignore response headers on resume requests
-            return;
-        }
-
-        readResponseHeaders(state, innerState, response);
-
-        try {
-            state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
-        } catch (DownloaderService.GenerateSaveFileError exc) {
-            throw new StopRequest(exc.mStatus, exc.mMessage);
-        }
-        try {
-            state.mStream = new FileOutputStream(state.mFilename);
-        } catch (FileNotFoundException exc) {
-            // make sure the directory exists
-            File pathFile = new File(Helpers.getSaveFilePath(mService));
-            try {
-                if (pathFile.mkdirs()) {
-                    state.mStream = new FileOutputStream(state.mFilename);
-                }
-            } catch (Exception ex) {
-                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-                        "while opening destination file: " + exc.toString(), exc);
-            }
-        }
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
-        }
-
-        updateDatabaseFromHeaders(state, innerState);
-        // check connectivity again now that we know the total size
-        checkConnectivity(state);
-    }
-
-    /**
+	private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
+			throws StopRequest {
+		if (innerState.mContinuingDownload) {
+			// ignore response headers on resume requests
+			return;
+		}
+
+		readResponseHeaders(state, innerState, response);
+
+		try {
+			state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes);
+		} catch (DownloaderService.GenerateSaveFileError exc) {
+			throw new StopRequest(exc.mStatus, exc.mMessage);
+		}
+		try {
+			state.mStream = new FileOutputStream(state.mFilename);
+		} catch (FileNotFoundException exc) {
+			// make sure the directory exists
+			File pathFile = new File(Helpers.getSaveFilePath(mService));
+			try {
+				if (pathFile.mkdirs()) {
+					state.mStream = new FileOutputStream(state.mFilename);
+				}
+			} catch (Exception ex) {
+				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+						"while opening destination file: " + exc.toString(), exc);
+			}
+		}
+		if (Constants.LOGV) {
+			Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
+		}
+
+		updateDatabaseFromHeaders(state, innerState);
+		// check connectivity again now that we know the total size
+		checkConnectivity(state);
+	}
+
+	/**
      * Update necessary database fields based on values of HTTP response headers
      * that have been read.
      */
-    private void updateDatabaseFromHeaders(State state, InnerState innerState) {
-        mInfo.mETag = innerState.mHeaderETag;
-        mDB.updateDownload(mInfo);
-    }
+	private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+		mInfo.mETag = innerState.mHeaderETag;
+		mDB.updateDownload(mInfo);
+	}
 
-    /**
+	/**
      * Read headers from the HTTP response and store them into local state.
      */
-    private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
-            throws StopRequest {
-        Header header = response.getFirstHeader("Content-Disposition");
-        if (header != null) {
-            innerState.mHeaderContentDisposition = header.getValue();
-        }
-        header = response.getFirstHeader("Content-Location");
-        if (header != null) {
-            innerState.mHeaderContentLocation = header.getValue();
-        }
-        header = response.getFirstHeader("ETag");
-        if (header != null) {
-            innerState.mHeaderETag = header.getValue();
-        }
-        String headerTransferEncoding = null;
-        header = response.getFirstHeader("Transfer-Encoding");
-        if (header != null) {
-            headerTransferEncoding = header.getValue();
-        }
-        String headerContentType = null;
-        header = response.getFirstHeader("Content-Type");
-        if (header != null) {
-            headerContentType = header.getValue();
-            if (!headerContentType.equals("application/vnd.android.obb")) {
-                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
-                        "file delivered with incorrect Mime type");
-            }
-        }
-
-        if (headerTransferEncoding == null) {
-            header = response.getFirstHeader("Content-Length");
-            if (header != null) {
-                innerState.mHeaderContentLength = header.getValue();
-                // this is always set from Market
-                long contentLength = Long.parseLong(innerState.mHeaderContentLength);
-                if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
-                    // we're most likely on a bad wifi connection -- we should
-                    // probably
-                    // also look at the mime type --- but the size mismatch is
-                    // enough
-                    // to tell us that something is wrong here
-                    Log.e(Constants.TAG, "Incorrect file size delivered.");
-                }
-            }
-        } else {
-            // Ignore content-length with transfer-encoding - 2616 4.4 3
-            if (Constants.LOGVV) {
-                Log.v(Constants.TAG,
-                        "ignoring content-length because of xfer-encoding");
-            }
-        }
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "Content-Disposition: " +
-                    innerState.mHeaderContentDisposition);
-            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
-            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
-            Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
-            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
-        }
-
-        boolean noSizeInfo = innerState.mHeaderContentLength == null
-                && (headerTransferEncoding == null
-                || !headerTransferEncoding.equalsIgnoreCase("chunked"));
-        if (noSizeInfo) {
-            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
-                    "can't know size of download, giving up");
-        }
-    }
-
-    /**
+	private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response)
+			throws StopRequest {
+		String value = response.getHeaderField("Content-Disposition");
+		if (value != null) {
+			innerState.mHeaderContentDisposition = value;
+		}
+		value = response.getHeaderField("Content-Location");
+		if (value != null) {
+			innerState.mHeaderContentLocation = value;
+		}
+		value = response.getHeaderField("ETag");
+		if (value != null) {
+			innerState.mHeaderETag = value;
+		}
+		String headerTransferEncoding = null;
+		value = response.getHeaderField("Transfer-Encoding");
+		if (value != null) {
+			headerTransferEncoding = value;
+		}
+		String headerContentType = null;
+		value = response.getHeaderField("Content-Type");
+		if (value != null) {
+			headerContentType = value;
+			if (!headerContentType.equals("application/vnd.android.obb")) {
+				throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
+						"file delivered with incorrect Mime type");
+			}
+		}
+
+		if (headerTransferEncoding == null) {
+			long contentLength = response.getContentLength();
+			if (value != null) {
+				// this is always set from Market
+				if (contentLength != -1 && contentLength != mInfo.mTotalBytes) {
+					// we're most likely on a bad wifi connection -- we should
+					// probably
+					// also look at the mime type --- but the size mismatch is
+					// enough
+					// to tell us that something is wrong here
+					Log.e(Constants.TAG, "Incorrect file size delivered.");
+				} else {
+					innerState.mHeaderContentLength = Long.toString(contentLength);
+				}
+			}
+		} else {
+			// Ignore content-length with transfer-encoding - 2616 4.4 3
+			if (Constants.LOGVV) {
+				Log.v(Constants.TAG,
+						"ignoring content-length because of xfer-encoding");
+			}
+		}
+		if (Constants.LOGVV) {
+			Log.v(Constants.TAG, "Content-Disposition: " +
+										 innerState.mHeaderContentDisposition);
+			Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
+			Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
+			Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
+			Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+		}
+
+		boolean noSizeInfo = innerState.mHeaderContentLength == null && (headerTransferEncoding == null || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+		if (noSizeInfo) {
+			throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+					"can't know size of download, giving up");
+		}
+	}
+
+	/**
      * Check the HTTP response status and handle anything unusual (e.g. not
      * 200/206).
      */
-    private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
-            throws StopRequest, RetryDownload {
-        int statusCode = response.getStatusLine().getStatusCode();
-        if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
-            handleServiceUnavailable(state, response);
-        }
-        if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
-            handleRedirect(state, response, statusCode);
-        }
-
-        int expectedStatus = innerState.mContinuingDownload ? 206
-                : DownloaderService.STATUS_SUCCESS;
-        if (statusCode != expectedStatus) {
-            handleOtherStatus(state, innerState, statusCode);
-        } else {
-            // no longer redirected
-            state.mRedirectCount = 0;
-        }
-    }
-
-    /**
+	private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode)
+			throws StopRequest, RetryDownload {
+		if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
+			handleServiceUnavailable(state, connection);
+		}
+		int expectedStatus = innerState.mContinuingDownload ? 206 : DownloaderService.STATUS_SUCCESS;
+		if (responseCode != expectedStatus) {
+			handleOtherStatus(state, innerState, responseCode);
+		} else {
+			// no longer redirected
+			state.mRedirectCount = 0;
+		}
+	}
+
+	/**
      * Handle a status that we don't know how to deal with properly.
      */
-    private void handleOtherStatus(State state, InnerState innerState, int statusCode)
-            throws StopRequest {
-        int finalStatus;
-        if (DownloaderService.isStatusError(statusCode)) {
-            finalStatus = statusCode;
-        } else if (statusCode >= 300 && statusCode < 400) {
-            finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
-        } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
-            finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
-        } else {
-            finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
-        }
-        throw new StopRequest(finalStatus, "http error " + statusCode);
-    }
-
-    /**
-     * Handle a 3xx redirect status.
-     */
-    private void handleRedirect(State state, HttpResponse response, int statusCode)
-            throws StopRequest, RetryDownload {
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
-        }
-        if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
-            throw new StopRequest(DownloaderService.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
-        }
-        Header header = response.getFirstHeader("Location");
-        if (header == null) {
-            return;
-        }
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "Location :" + header.getValue());
-        }
-
-        String newUri;
-        try {
-            newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
-        } catch (URISyntaxException ex) {
-            if (Constants.LOGV) {
-                Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
-                        + " for " + mInfo.mUri);
-            }
-            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
-                    "Couldn't resolve redirect URI");
-        }
-        ++state.mRedirectCount;
-        state.mRequestUri = newUri;
-        if (statusCode == 301 || statusCode == 303) {
-            // use the new URI for all future requests (should a retry/resume be
-            // necessary)
-            state.mNewUri = newUri;
-        }
-        throw new RetryDownload();
-    }
-
-    /**
+	private void handleOtherStatus(State state, InnerState innerState, int statusCode)
+			throws StopRequest {
+		int finalStatus;
+		if (DownloaderService.isStatusError(statusCode)) {
+			finalStatus = statusCode;
+		} else if (statusCode >= 300 && statusCode < 400) {
+			finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT;
+		} else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) {
+			finalStatus = DownloaderService.STATUS_CANNOT_RESUME;
+		} else {
+			finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE;
+		}
+		throw new StopRequest(finalStatus, "http error " + statusCode);
+	}
+
+	/**
      * Add headers for this download to the HTTP request to allow for resume.
      */
-    private void addRequestHeaders(InnerState innerState, HttpGet request) {
-        if (innerState.mContinuingDownload) {
-            if (innerState.mHeaderETag != null) {
-                request.addHeader("If-Match", innerState.mHeaderETag);
-            }
-            request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
-        }
-    }
-
-    /**
+	private void addRequestHeaders(InnerState innerState, HttpURLConnection request) {
+		if (innerState.mContinuingDownload) {
+			if (innerState.mHeaderETag != null) {
+				request.setRequestProperty("If-Match", innerState.mHeaderETag);
+			}
+			request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-");
+		}
+	}
+
+	/**
      * Handle a 503 Service Unavailable status by processing the Retry-After
      * header.
      */
-    private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
-        if (Constants.LOGVV) {
-            Log.v(Constants.TAG, "got HTTP response code 503");
-        }
-        state.mCountRetry = true;
-        Header header = response.getFirstHeader("Retry-After");
-        if (header != null) {
-            try {
-                if (Constants.LOGVV) {
-                    Log.v(Constants.TAG, "Retry-After :" + header.getValue());
-                }
-                state.mRetryAfter = Integer.parseInt(header.getValue());
-                if (state.mRetryAfter < 0) {
-                    state.mRetryAfter = 0;
-                } else {
-                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
-                        state.mRetryAfter = Constants.MIN_RETRY_AFTER;
-                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
-                        state.mRetryAfter = Constants.MAX_RETRY_AFTER;
-                    }
-                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
-                    state.mRetryAfter *= 1000;
-                }
-            } catch (NumberFormatException ex) {
-                // ignored - retryAfter stays 0 in this case.
-            }
-        }
-        throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
-                "got 503 Service Unavailable, will retry later");
-    }
-
-    /**
+	private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest {
+		if (Constants.LOGVV) {
+			Log.v(Constants.TAG, "got HTTP response code 503");
+		}
+		state.mCountRetry = true;
+		String retryAfterValue = connection.getHeaderField("Retry-After");
+		if (retryAfterValue != null) {
+			try {
+				if (Constants.LOGVV) {
+					Log.v(Constants.TAG, "Retry-After :" + retryAfterValue);
+				}
+				state.mRetryAfter = Integer.parseInt(retryAfterValue);
+				if (state.mRetryAfter < 0) {
+					state.mRetryAfter = 0;
+				} else {
+					if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+						state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+					} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+						state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+					}
+					state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+					state.mRetryAfter *= 1000;
+				}
+			} catch (NumberFormatException ex) {
+				// ignored - retryAfter stays 0 in this case.
+			}
+		}
+		throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY,
+				"got 503 Service Unavailable, will retry later");
+	}
+
+	/**
      * Send the request to the server, handling any I/O exceptions.
      */
-    private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
-            throws StopRequest {
-        try {
-            return client.execute(request);
-        } catch (IllegalArgumentException ex) {
-            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
-                    "while trying to execute request: " + ex.toString(), ex);
-        } catch (IOException ex) {
-            logNetworkState();
-            throw new StopRequest(getFinalStatusForHttpError(state),
-                    "while trying to execute request: " + ex.toString(), ex);
-        }
-    }
-
-    private int getFinalStatusForHttpError(State state) {
-        if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
-            return DownloaderService.STATUS_WAITING_FOR_NETWORK;
-        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
-            state.mCountRetry = true;
-            return DownloaderService.STATUS_WAITING_TO_RETRY;
-        } else {
-            Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
-            return DownloaderService.STATUS_HTTP_DATA_ERROR;
-        }
-    }
-
-    /**
+	private int sendRequest(State state, HttpURLConnection request)
+			throws StopRequest {
+		try {
+			return request.getResponseCode();
+		} catch (IllegalArgumentException ex) {
+			throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR,
+					"while trying to execute request: " + ex.toString(), ex);
+		} catch (IOException ex) {
+			logNetworkState();
+			throw new StopRequest(getFinalStatusForHttpError(state),
+					"while trying to execute request: " + ex.toString(), ex);
+		}
+	}
+
+	private int getFinalStatusForHttpError(State state) {
+		if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) {
+			return DownloaderService.STATUS_WAITING_FOR_NETWORK;
+		} else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+			state.mCountRetry = true;
+			return DownloaderService.STATUS_WAITING_TO_RETRY;
+		} else {
+			Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed);
+			return DownloaderService.STATUS_HTTP_DATA_ERROR;
+		}
+	}
+
+	/**
      * Prepare the destination file to receive data. If the file already exists,
      * we'll set up appropriately for resumption.
      */
-    private void setupDestinationFile(State state, InnerState innerState)
-            throws StopRequest {
-        if (state.mFilename != null) { // only true if we've already run a
-                                       // thread for this download
-            if (!Helpers.isFilenameValid(state.mFilename)) {
-                // this should never happen
-                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-                        "found invalid internal destination filename");
-            }
-            // We're resuming a download that got interrupted
-            File f = new File(state.mFilename);
-            if (f.exists()) {
-                long fileLength = f.length();
-                if (fileLength == 0) {
-                    // The download hadn't actually started, we can restart from
-                    // scratch
-                    f.delete();
-                    state.mFilename = null;
-                } else if (mInfo.mETag == null) {
-                    // This should've been caught upon failure
-                    f.delete();
-                    throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
-                            "Trying to resume a download that can't be resumed");
-                } else {
-                    // All right, we'll be able to resume this download
-                    try {
-                        state.mStream = new FileOutputStream(state.mFilename, true);
-                    } catch (FileNotFoundException exc) {
-                        throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
-                                "while opening destination for resuming: " + exc.toString(), exc);
-                    }
-                    innerState.mBytesSoFar = (int) fileLength;
-                    if (mInfo.mTotalBytes != -1) {
-                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
-                    }
-                    innerState.mHeaderETag = mInfo.mETag;
-                    innerState.mContinuingDownload = true;
-                }
-            }
-        }
-
-        if (state.mStream != null) {
-            closeDestination(state);
-        }
-    }
-
-    /**
+	private void setupDestinationFile(State state, InnerState innerState)
+			throws StopRequest {
+		if (state.mFilename != null) { // only true if we've already run a
+			// thread for this download
+			if (!Helpers.isFilenameValid(state.mFilename)) {
+				// this should never happen
+				throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+						"found invalid internal destination filename");
+			}
+			// We're resuming a download that got interrupted
+			File f = new File(state.mFilename);
+			if (f.exists()) {
+				long fileLength = f.length();
+				if (fileLength == 0) {
+					// The download hadn't actually started, we can restart from
+					// scratch
+					f.delete();
+					state.mFilename = null;
+				} else if (mInfo.mETag == null) {
+					// This should've been caught upon failure
+					f.delete();
+					throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME,
+							"Trying to resume a download that can't be resumed");
+				} else {
+					// All right, we'll be able to resume this download
+					try {
+						state.mStream = new FileOutputStream(state.mFilename, true);
+					} catch (FileNotFoundException exc) {
+						throw new StopRequest(DownloaderService.STATUS_FILE_ERROR,
+								"while opening destination for resuming: " + exc.toString(), exc);
+					}
+					innerState.mBytesSoFar = (int)fileLength;
+					if (mInfo.mTotalBytes != -1) {
+						innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
+					}
+					innerState.mHeaderETag = mInfo.mETag;
+					innerState.mContinuingDownload = true;
+				}
+			}
+		}
+
+		if (state.mStream != null) {
+			closeDestination(state);
+		}
+	}
+
+	/**
      * Stores information about the completed download, and notifies the
      * initiating application.
      */
-    private void notifyDownloadCompleted(
-            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
-            String filename) {
-        updateDownloadDatabase(
-                status, countRetry, retryAfter, redirectCount, gotData, filename);
-        if (DownloaderService.isStatusCompleted(status)) {
-            // TBD: send status update?
-        }
-    }
-
-    private void updateDownloadDatabase(
-            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
-            String filename) {
-        mInfo.mStatus = status;
-        mInfo.mRetryAfter = retryAfter;
-        mInfo.mRedirectCount = redirectCount;
-        mInfo.mLastMod = System.currentTimeMillis();
-        if (!countRetry) {
-            mInfo.mNumFailed = 0;
-        } else if (gotData) {
-            mInfo.mNumFailed = 1;
-        } else {
-            mInfo.mNumFailed++;
-        }
-        mDB.updateDownload(mInfo);
-    }
-
+	private void notifyDownloadCompleted(
+			int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+			String filename) {
+		updateDownloadDatabase(
+				status, countRetry, retryAfter, redirectCount, gotData, filename);
+		if (DownloaderService.isStatusCompleted(status)) {
+			// TBD: send status update?
+		}
+	}
+
+	private void updateDownloadDatabase(
+			int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+			String filename) {
+		mInfo.mStatus = status;
+		mInfo.mRetryAfter = retryAfter;
+		mInfo.mRedirectCount = redirectCount;
+		mInfo.mLastMod = System.currentTimeMillis();
+		if (!countRetry) {
+			mInfo.mNumFailed = 0;
+		} else if (gotData) {
+			mInfo.mNumFailed = 1;
+		} else {
+			mInfo.mNumFailed++;
+		}
+		mDB.updateDownload(mInfo);
+	}
 }

+ 960 - 981
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java

@@ -29,6 +29,7 @@ import com.google.android.vending.licensing.LicenseChecker;
 import com.google.android.vending.licensing.LicenseCheckerCallback;
 import com.google.android.vending.licensing.Policy;
 
+import android.annotation.SuppressLint;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -61,82 +62,82 @@ import java.io.File;
  */
 public abstract class DownloaderService extends CustomIntentService implements IDownloaderService {
 
-    public DownloaderService() {
-        super("LVLDownloadService");
-    }
+	public DownloaderService() {
+		super("LVLDownloadService");
+	}
 
-    private static final String LOG_TAG = "LVLDL";
+	private static final String LOG_TAG = "LVLDL";
 
-    // the following NETWORK_* constants are used to indicates specific reasons
-    // for disallowing a
-    // download from using a network, since specific causes can require special
-    // handling
+	// the following NETWORK_* constants are used to indicates specific reasons
+	// for disallowing a
+	// download from using a network, since specific causes can require special
+	// handling
 
-    /**
+	/**
      * The network is usable for the given download.
      */
-    public static final int NETWORK_OK = 1;
+	public static final int NETWORK_OK = 1;
 
-    /**
+	/**
      * There is no network connectivity.
      */
-    public static final int NETWORK_NO_CONNECTION = 2;
+	public static final int NETWORK_NO_CONNECTION = 2;
 
-    /**
+	/**
      * The download exceeds the maximum size for this network.
      */
-    public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
+	public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
 
-    /**
+	/**
      * The download exceeds the recommended maximum size for this network, the
      * user must confirm for this download to proceed without WiFi.
      */
-    public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
+	public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
 
-    /**
+	/**
      * The current connection is roaming, and the download can't proceed over a
      * roaming connection.
      */
-    public static final int NETWORK_CANNOT_USE_ROAMING = 5;
+	public static final int NETWORK_CANNOT_USE_ROAMING = 5;
 
-    /**
+	/**
      * The app requesting the download specific that it can't use the current
      * network connection.
      */
-    public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
+	public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
 
-    /**
+	/**
      * For intents used to notify the user that a download exceeds a size
      * threshold, if this extra is true, WiFi is required for this download
      * size; otherwise, it is only recommended.
      */
-    public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
-    public static final String EXTRA_FILE_NAME = "downloadId";
+	public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
+	public static final String EXTRA_FILE_NAME = "downloadId";
 
-    /**
+	/**
      * Used with DOWNLOAD_STATUS
      */
-    public static final String EXTRA_STATUS_STATE = "ESS";
-    public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS";
-    public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS";
-    public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP";
-    public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP";
+	public static final String EXTRA_STATUS_STATE = "ESS";
+	public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS";
+	public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS";
+	public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP";
+	public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP";
 
-    public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged";
+	public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged";
 
-    /**
+	/**
      * Broadcast intent action sent by the download manager when a download
      * completes.
      */
-    public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE";
+	public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE";
 
-    /**
+	/**
      * Broadcast intent action sent by the download manager when download status
      * changes.
      */
-    public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS";
+	public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS";
 
-    /*
+	/*
      * Lists the states that the download manager can set on a download to
      * notify applications of the download progress. The codes follow the HTTP
      * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not
@@ -144,503 +145,498 @@ public abstract class DownloaderService extends CustomIntentService implements I
      * errors
      */
 
-    /**
+	/**
      * Returns whether the status is informational (i.e. 1xx).
      */
-    public static boolean isStatusInformational(int status) {
-        return (status >= 100 && status < 200);
-    }
+	public static boolean isStatusInformational(int status) {
+		return (status >= 100 && status < 200);
+	}
 
-    /**
+	/**
      * Returns whether the status is a success (i.e. 2xx).
      */
-    public static boolean isStatusSuccess(int status) {
-        return (status >= 200 && status < 300);
-    }
+	public static boolean isStatusSuccess(int status) {
+		return (status >= 200 && status < 300);
+	}
 
-    /**
+	/**
      * Returns whether the status is an error (i.e. 4xx or 5xx).
      */
-    public static boolean isStatusError(int status) {
-        return (status >= 400 && status < 600);
-    }
+	public static boolean isStatusError(int status) {
+		return (status >= 400 && status < 600);
+	}
 
-    /**
+	/**
      * Returns whether the status is a client error (i.e. 4xx).
      */
-    public static boolean isStatusClientError(int status) {
-        return (status >= 400 && status < 500);
-    }
+	public static boolean isStatusClientError(int status) {
+		return (status >= 400 && status < 500);
+	}
 
-    /**
+	/**
      * Returns whether the status is a server error (i.e. 5xx).
      */
-    public static boolean isStatusServerError(int status) {
-        return (status >= 500 && status < 600);
-    }
+	public static boolean isStatusServerError(int status) {
+		return (status >= 500 && status < 600);
+	}
 
-    /**
+	/**
      * Returns whether the download has completed (either with success or
      * error).
      */
-    public static boolean isStatusCompleted(int status) {
-        return (status >= 200 && status < 300)
-                || (status >= 400 && status < 600);
-    }
+	public static boolean isStatusCompleted(int status) {
+		return (status >= 200 && status < 300) || (status >= 400 && status < 600);
+	}
 
-    /**
+	/**
      * This download hasn't stated yet
      */
-    public static final int STATUS_PENDING = 190;
+	public static final int STATUS_PENDING = 190;
 
-    /**
+	/**
      * This download has started
      */
-    public static final int STATUS_RUNNING = 192;
+	public static final int STATUS_RUNNING = 192;
 
-    /**
+	/**
      * This download has been paused by the owning app.
      */
-    public static final int STATUS_PAUSED_BY_APP = 193;
+	public static final int STATUS_PAUSED_BY_APP = 193;
 
-    /**
+	/**
      * This download encountered some network error and is waiting before
      * retrying the request.
      */
-    public static final int STATUS_WAITING_TO_RETRY = 194;
+	public static final int STATUS_WAITING_TO_RETRY = 194;
 
-    /**
+	/**
      * This download is waiting for network connectivity to proceed.
      */
-    public static final int STATUS_WAITING_FOR_NETWORK = 195;
+	public static final int STATUS_WAITING_FOR_NETWORK = 195;
 
-    /**
+	/**
      * This download is waiting for a Wi-Fi connection to proceed or for
      * permission to download over cellular.
      */
-    public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196;
+	public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196;
 
-    /**
+	/**
      * This download is waiting for a Wi-Fi connection to proceed.
      */
-    public static final int STATUS_QUEUED_FOR_WIFI = 197;
+	public static final int STATUS_QUEUED_FOR_WIFI = 197;
 
-    /**
+	/**
      * 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.
-     * 
+     *
      * @hide
      */
-    public static final int STATUS_SUCCESS = 200;
+	public static final int STATUS_SUCCESS = 200;
 
-    /**
+	/**
      * The requested URL is no longer available
      */
-    public static final int STATUS_FORBIDDEN = 403;
+	public static final int STATUS_FORBIDDEN = 403;
 
-    /**
+	/**
      * The file was delivered incorrectly
      */
-    public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487;
+	public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487;
 
-    /**
+	/**
      * 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
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    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.
-     * 
+     *
      * @hide
      */
-    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
+	public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
 
-    /**
+	/**
      * This download is allowed to run.
-     * 
+     *
      * @hide
      */
-    public static final int CONTROL_RUN = 0;
+	public static final int CONTROL_RUN = 0;
 
-    /**
+	/**
      * This download must pause at the first opportunity.
-     * 
+     *
      * @hide
      */
-    public static final int CONTROL_PAUSED = 1;
+	public static final int CONTROL_PAUSED = 1;
 
-    /**
+	/**
      * This download is visible but only shows in the notifications while it's
      * in progress.
-     * 
+     *
      * @hide
      */
-    public static final int VISIBILITY_VISIBLE = 0;
+	public static final int VISIBILITY_VISIBLE = 0;
 
-    /**
+	/**
      * This download is visible and shows in the notifications while in progress
      * and after completion.
-     * 
+     *
      * @hide
      */
-    public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
+	public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
 
-    /**
+	/**
      * This download doesn't show in the UI or in the notifications.
-     * 
+     *
      * @hide
      */
-    public static final int VISIBILITY_HIDDEN = 2;
+	public static final int VISIBILITY_HIDDEN = 2;
 
-    /**
-     * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+	/**
+     * Bit flag for setAllowedNetworkTypes corresponding to
      * {@link ConnectivityManager#TYPE_MOBILE}.
      */
-    public static final int NETWORK_MOBILE = 1 << 0;
+	public static final int NETWORK_MOBILE = 1 << 0;
 
-    /**
-     * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
+	/**
+     * Bit flag for setAllowedNetworkTypes corresponding to
      * {@link ConnectivityManager#TYPE_WIFI}.
      */
-    public static final int NETWORK_WIFI = 1 << 1;
+	public static final int NETWORK_WIFI = 1 << 1;
 
-    private final static String TEMP_EXT = ".tmp";
+	private final static String TEMP_EXT = ".tmp";
 
-    /**
+	/**
      * Service thread status
      */
-    private static boolean sIsRunning;
+	private static boolean sIsRunning;
 
-    @Override
-    public IBinder onBind(Intent paramIntent) {
-        Log.d(Constants.TAG, "Service Bound");
-        return this.mServiceMessenger.getBinder();
-    }
+	@Override
+	public IBinder onBind(Intent paramIntent) {
+		Log.d(Constants.TAG, "Service Bound");
+		return this.mServiceMessenger.getBinder();
+	}
 
-    /**
+	/**
      * Network state.
      */
-    private boolean mIsConnected;
-    private boolean mIsFailover;
-    private boolean mIsCellularConnection;
-    private boolean mIsRoaming;
-    private boolean mIsAtLeast3G;
-    private boolean mIsAtLeast4G;
-    private boolean mStateChanged;
+	private boolean mIsConnected;
+	private boolean mIsFailover;
+	private boolean mIsCellularConnection;
+	private boolean mIsRoaming;
+	private boolean mIsAtLeast3G;
+	private boolean mIsAtLeast4G;
+	private boolean mStateChanged;
 
-    /**
+	/**
      * Download state
      */
-    private int mControl;
-    private int mStatus;
+	private int mControl;
+	private int mStatus;
 
-    public boolean isWiFi() {
-        return mIsConnected && !mIsCellularConnection;
-    }
+	public boolean isWiFi() {
+		return mIsConnected && !mIsCellularConnection;
+	}
 
-    /**
+	/**
      * Bindings to important services
      */
-    private ConnectivityManager mConnectivityManager;
-    private WifiManager mWifiManager;
+	private ConnectivityManager mConnectivityManager;
+	private WifiManager mWifiManager;
 
-    /**
+	/**
      * Package we are downloading for (defaults to package of application)
      */
-    private PackageInfo mPackageInfo;
+	private PackageInfo mPackageInfo;
 
-    /**
+	/**
      * Byte counts
      */
-    long mBytesSoFar;
-    long mTotalLength;
-    int mFileCount;
+	long mBytesSoFar;
+	long mTotalLength;
+	int mFileCount;
 
-    /**
+	/**
      * Used for calculating time remaining and speed
      */
-    long mBytesAtSample;
-    long mMillisecondsAtSample;
-    float mAverageDownloadSpeed;
+	long mBytesAtSample;
+	long mMillisecondsAtSample;
+	float mAverageDownloadSpeed;
 
-    /**
+	/**
      * Our binding to the network state broadcasts
      */
-    private BroadcastReceiver mConnReceiver;
-    final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this);
-    final private Messenger mServiceMessenger = mServiceStub.getMessenger();
-    private Messenger mClientMessenger;
-    private DownloadNotification mNotification;
-    private PendingIntent mPendingIntent;
-    private PendingIntent mAlarmIntent;
+	private BroadcastReceiver mConnReceiver;
+	final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this);
+	final private Messenger mServiceMessenger = mServiceStub.getMessenger();
+	private Messenger mClientMessenger;
+	private DownloadNotification mNotification;
+	private PendingIntent mPendingIntent;
+	private PendingIntent mAlarmIntent;
 
-    /**
+	/**
      * Updates the network type based upon the type and subtype returned from
      * the connectivity manager. Subtype is only used for cellular signals.
-     * 
+     *
      * @param type
      * @param subType
      */
-    private void updateNetworkType(int type, int subType) {
-        switch (type) {
-            case ConnectivityManager.TYPE_WIFI:
-            case ConnectivityManager.TYPE_ETHERNET:
-            case ConnectivityManager.TYPE_BLUETOOTH:
-                mIsCellularConnection = false;
-                mIsAtLeast3G = false;
-                mIsAtLeast4G = false;
-                break;
-            case ConnectivityManager.TYPE_WIMAX:
-                mIsCellularConnection = true;
-                mIsAtLeast3G = true;
-                mIsAtLeast4G = true;
-                break;
-            case ConnectivityManager.TYPE_MOBILE:
-                mIsCellularConnection = true;
-                switch (subType) {
-                    case TelephonyManager.NETWORK_TYPE_1xRTT:
-                    case TelephonyManager.NETWORK_TYPE_CDMA:
-                    case TelephonyManager.NETWORK_TYPE_EDGE:
-                    case TelephonyManager.NETWORK_TYPE_GPRS:
-                    case TelephonyManager.NETWORK_TYPE_IDEN:
-                        mIsAtLeast3G = false;
-                        mIsAtLeast4G = false;
-                        break;
-                    case TelephonyManager.NETWORK_TYPE_HSDPA:
-                    case TelephonyManager.NETWORK_TYPE_HSUPA:
-                    case TelephonyManager.NETWORK_TYPE_HSPA:
-                    case TelephonyManager.NETWORK_TYPE_EVDO_0:
-                    case TelephonyManager.NETWORK_TYPE_EVDO_A:
-                    case TelephonyManager.NETWORK_TYPE_UMTS:
-                        mIsAtLeast3G = true;
-                        mIsAtLeast4G = false;
-                        break;
-                    case TelephonyManager.NETWORK_TYPE_LTE: // 4G
-                    case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop
-                                                              // with 4G
-                    case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but
-                                                              // marketed as
-                                                              // 4G
-                        mIsAtLeast3G = true;
-                        mIsAtLeast4G = true;
-                        break;
-                    default:
-                        mIsCellularConnection = false;
-                        mIsAtLeast3G = false;
-                        mIsAtLeast4G = false;
-                }
-        }
-    }
-
-    private void updateNetworkState(NetworkInfo info) {
-        boolean isConnected = mIsConnected;
-        boolean isFailover = mIsFailover;
-        boolean isCellularConnection = mIsCellularConnection;
-        boolean isRoaming = mIsRoaming;
-        boolean isAtLeast3G = mIsAtLeast3G;
-        if (null != info) {
-            mIsRoaming = info.isRoaming();
-            mIsFailover = info.isFailover();
-            mIsConnected = info.isConnected();
-            updateNetworkType(info.getType(), info.getSubtype());
-        } else {
-            mIsRoaming = false;
-            mIsFailover = false;
-            mIsConnected = false;
-            updateNetworkType(-1, -1);
-        }
-        mStateChanged = (mStateChanged || isConnected != mIsConnected
-                || isFailover != mIsFailover
-                || isCellularConnection != mIsCellularConnection
-                || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G);
-        if (Constants.LOGVV) {
-            if (mStateChanged) {
-                Log.v(LOG_TAG, "Network state changed: ");
-                Log.v(LOG_TAG, "Starting State: " +
-                        (isConnected ? "Connected " : "Not Connected ") +
-                        (isCellularConnection ? "Cellular " : "WiFi ") +
-                        (isRoaming ? "Roaming " : "Local ") +
-                        (isAtLeast3G ? "3G+ " : "<3G "));
-                Log.v(LOG_TAG, "Ending State: " +
-                        (mIsConnected ? "Connected " : "Not Connected ") +
-                        (mIsCellularConnection ? "Cellular " : "WiFi ") +
-                        (mIsRoaming ? "Roaming " : "Local ") +
-                        (mIsAtLeast3G ? "3G+ " : "<3G "));
-
-                if (isServiceRunning()) {
-                    if (mIsRoaming) {
-                        mStatus = STATUS_WAITING_FOR_NETWORK;
-                        mControl = CONTROL_PAUSED;
-                    } else if (mIsCellularConnection) {
-                        DownloadsDB db = DownloadsDB.getDB(this);
-                        int flags = db.getFlags();
-                        if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
-                            mStatus = STATUS_QUEUED_FOR_WIFI;
-                            mControl = CONTROL_PAUSED;
-                        }
-                    }
-                }
-
-            }
-        }
-    }
-
-    /**
+	private void updateNetworkType(int type, int subType) {
+		switch (type) {
+			case ConnectivityManager.TYPE_WIFI:
+			case ConnectivityManager.TYPE_ETHERNET:
+			case ConnectivityManager.TYPE_BLUETOOTH:
+				mIsCellularConnection = false;
+				mIsAtLeast3G = false;
+				mIsAtLeast4G = false;
+				break;
+			case ConnectivityManager.TYPE_WIMAX:
+				mIsCellularConnection = true;
+				mIsAtLeast3G = true;
+				mIsAtLeast4G = true;
+				break;
+			case ConnectivityManager.TYPE_MOBILE:
+				mIsCellularConnection = true;
+				switch (subType) {
+					case TelephonyManager.NETWORK_TYPE_1xRTT:
+					case TelephonyManager.NETWORK_TYPE_CDMA:
+					case TelephonyManager.NETWORK_TYPE_EDGE:
+					case TelephonyManager.NETWORK_TYPE_GPRS:
+					case TelephonyManager.NETWORK_TYPE_IDEN:
+						mIsAtLeast3G = false;
+						mIsAtLeast4G = false;
+						break;
+					case TelephonyManager.NETWORK_TYPE_HSDPA:
+					case TelephonyManager.NETWORK_TYPE_HSUPA:
+					case TelephonyManager.NETWORK_TYPE_HSPA:
+					case TelephonyManager.NETWORK_TYPE_EVDO_0:
+					case TelephonyManager.NETWORK_TYPE_EVDO_A:
+					case TelephonyManager.NETWORK_TYPE_UMTS:
+						mIsAtLeast3G = true;
+						mIsAtLeast4G = false;
+						break;
+					case TelephonyManager.NETWORK_TYPE_LTE: // 4G
+					case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop
+							// with 4G
+					case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but
+							// marketed as
+							// 4G
+						mIsAtLeast3G = true;
+						mIsAtLeast4G = true;
+						break;
+					default:
+						mIsCellularConnection = false;
+						mIsAtLeast3G = false;
+						mIsAtLeast4G = false;
+				}
+		}
+	}
+
+	private void updateNetworkState(NetworkInfo info) {
+		boolean isConnected = mIsConnected;
+		boolean isFailover = mIsFailover;
+		boolean isCellularConnection = mIsCellularConnection;
+		boolean isRoaming = mIsRoaming;
+		boolean isAtLeast3G = mIsAtLeast3G;
+		if (null != info) {
+			mIsRoaming = info.isRoaming();
+			mIsFailover = info.isFailover();
+			mIsConnected = info.isConnected();
+			updateNetworkType(info.getType(), info.getSubtype());
+		} else {
+			mIsRoaming = false;
+			mIsFailover = false;
+			mIsConnected = false;
+			updateNetworkType(-1, -1);
+		}
+		mStateChanged = (mStateChanged || isConnected != mIsConnected || isFailover != mIsFailover || isCellularConnection != mIsCellularConnection || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G);
+		if (Constants.LOGVV) {
+			if (mStateChanged) {
+				Log.v(LOG_TAG, "Network state changed: ");
+				Log.v(LOG_TAG, "Starting State: " +
+									   (isConnected ? "Connected " : "Not Connected ") +
+									   (isCellularConnection ? "Cellular " : "WiFi ") +
+									   (isRoaming ? "Roaming " : "Local ") +
+									   (isAtLeast3G ? "3G+ " : "<3G "));
+				Log.v(LOG_TAG, "Ending State: " +
+									   (mIsConnected ? "Connected " : "Not Connected ") +
+									   (mIsCellularConnection ? "Cellular " : "WiFi ") +
+									   (mIsRoaming ? "Roaming " : "Local ") +
+									   (mIsAtLeast3G ? "3G+ " : "<3G "));
+
+				if (isServiceRunning()) {
+					if (mIsRoaming) {
+						mStatus = STATUS_WAITING_FOR_NETWORK;
+						mControl = CONTROL_PAUSED;
+					} else if (mIsCellularConnection) {
+						DownloadsDB db = DownloadsDB.getDB(this);
+						int flags = db.getFlags();
+						if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
+							mStatus = STATUS_QUEUED_FOR_WIFI;
+							mControl = CONTROL_PAUSED;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
      * Polls the network state, setting the flags appropriately.
      */
-    void pollNetworkState() {
-        if (null == mConnectivityManager) {
-            mConnectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        }
-        if (null == mWifiManager) {
-            mWifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
-        }
-        if (mConnectivityManager == null) {
-            Log.w(Constants.TAG,
-                    "couldn't get connectivity manager to poll network state");
-        } else {
-            NetworkInfo activeInfo = mConnectivityManager
-                    .getActiveNetworkInfo();
-            updateNetworkState(activeInfo);
-        }
-    }
-
-    public static final int NO_DOWNLOAD_REQUIRED = 0;
-    public static final int LVL_CHECK_REQUIRED = 1;
-    public static final int DOWNLOAD_REQUIRED = 2;
-
-    public static final String EXTRA_PACKAGE_NAME = "EPN";
-    public static final String EXTRA_PENDING_INTENT = "EPI";
-    public static final String EXTRA_MESSAGE_HANDLER = "EMH";
-
-    /**
+	void pollNetworkState() {
+		if (null == mConnectivityManager) {
+			mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+		}
+		if (null == mWifiManager) {
+			mWifiManager = (WifiManager)getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+		}
+		if (mConnectivityManager == null) {
+			Log.w(Constants.TAG,
+					"couldn't get connectivity manager to poll network state");
+		} else {
+			@SuppressLint("MissingPermission")
+			NetworkInfo activeInfo = mConnectivityManager
+											 .getActiveNetworkInfo();
+			updateNetworkState(activeInfo);
+		}
+	}
+
+	public static final int NO_DOWNLOAD_REQUIRED = 0;
+	public static final int LVL_CHECK_REQUIRED = 1;
+	public static final int DOWNLOAD_REQUIRED = 2;
+
+	public static final String EXTRA_PACKAGE_NAME = "EPN";
+	public static final String EXTRA_PENDING_INTENT = "EPI";
+	public static final String EXTRA_MESSAGE_HANDLER = "EMH";
+
+	/**
      * Returns true if the LVL check is required
-     * 
+     *
      * @param db a downloads DB synchronized with the latest state
      * @param pi the package info for the project
      * @return returns true if the filenames need to be returned
      */
-    private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) {
-        // we need to update the LVL check and get a successful status to
-        // proceed
-        if (db.mVersionCode != pi.versionCode) {
-            return true;
-        }
-        return false;
-    }
+	private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) {
+		// we need to update the LVL check and get a successful status to
+		// proceed
+		if (db.mVersionCode != pi.versionCode) {
+			return true;
+		}
+		return false;
+	}
 
-    /**
+	/**
      * Careful! Only use this internally.
-     * 
+     *
      * @return whether we think the service is running
      */
-    private static synchronized boolean isServiceRunning() {
-        return sIsRunning;
-    }
-
-    private static synchronized void setServiceRunning(boolean isRunning) {
-        sIsRunning = isRunning;
-    }
-
-    public static int startDownloadServiceIfRequired(Context context,
-            Intent intent, Class<?> serviceClass) throws NameNotFoundException {
-        final PendingIntent pendingIntent = (PendingIntent) intent
-                .getParcelableExtra(EXTRA_PENDING_INTENT);
-        return startDownloadServiceIfRequired(context, pendingIntent,
-                serviceClass);
-    }
-
-    public static int startDownloadServiceIfRequired(Context context,
-            PendingIntent pendingIntent, Class<?> serviceClass)
-            throws NameNotFoundException
-    {
-        String packageName = context.getPackageName();
-        String className = serviceClass.getName();
-
-        return startDownloadServiceIfRequired(context, pendingIntent,
-                packageName, className);
-    }
-
-    /**
+	private static synchronized boolean isServiceRunning() {
+		return sIsRunning;
+	}
+
+	private static synchronized void setServiceRunning(boolean isRunning) {
+		sIsRunning = isRunning;
+	}
+
+	public static int startDownloadServiceIfRequired(Context context,
+			Intent intent, Class<?> serviceClass) throws NameNotFoundException {
+		final PendingIntent pendingIntent = (PendingIntent)intent
+													.getParcelableExtra(EXTRA_PENDING_INTENT);
+		return startDownloadServiceIfRequired(context, pendingIntent,
+				serviceClass);
+	}
+
+	public static int startDownloadServiceIfRequired(Context context,
+			PendingIntent pendingIntent, Class<?> serviceClass)
+			throws NameNotFoundException {
+		String packageName = context.getPackageName();
+		String className = serviceClass.getName();
+
+		return startDownloadServiceIfRequired(context, pendingIntent,
+				packageName, className);
+	}
+
+	/**
      * 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, checks
@@ -652,690 +648,673 @@ public abstract class DownloaderService extends CustomIntentService implements I
      * 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
-     * @param thisIntent
+     * @param pendingIntent
      * @return true if the app should wait for more guidance from the
      *         downloader, false if the app can continue
      * @throws NameNotFoundException
      */
-    public static int startDownloadServiceIfRequired(Context context,
-            PendingIntent pendingIntent, String classPackage, String className)
-            throws NameNotFoundException {
-        // first: do we need to do an LVL update?
-        // we begin by getting our APK version from the package manager
-        final PackageInfo pi = context.getPackageManager().getPackageInfo(
-                context.getPackageName(), 0);
-
-        int status = NO_DOWNLOAD_REQUIRED;
-
-        // the database automatically reads the metadata for version code
-        // and download status when the instance is created
-        DownloadsDB db = DownloadsDB.getDB(context);
-
-        // we need to update the LVL check and get a successful status to
-        // proceed
-        if (isLVLCheckRequired(db, pi)) {
-            status = LVL_CHECK_REQUIRED;
-        }
-        // we don't have to update LVL. do we still have a download to start?
-        if (db.mStatus == 0) {
-            DownloadInfo[] infos = db.getDownloads();
-            if (null != infos) {
-                for (DownloadInfo info : infos) {
-                    if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) {
-                        status = DOWNLOAD_REQUIRED;
-                        db.updateStatus(-1);
-                        break;
-                    }
-                }
-            }
-        } else {
-            status = DOWNLOAD_REQUIRED;
-        }
-        switch (status) {
-            case DOWNLOAD_REQUIRED:
-            case LVL_CHECK_REQUIRED:
-                Intent fileIntent = new Intent();
-                fileIntent.setClassName(classPackage, className);
-                fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
-                context.startService(fileIntent);
-                break;
-        }
-        return status;
-    }
-
-    @Override
-    public void requestAbortDownload() {
-        mControl = CONTROL_PAUSED;
-        mStatus = STATUS_CANCELED;
-    }
-
-    @Override
-    public void requestPauseDownload() {
-        mControl = CONTROL_PAUSED;
-        mStatus = STATUS_PAUSED_BY_APP;
-    }
-
-    @Override
-    public void setDownloadFlags(int flags) {
-        DownloadsDB.getDB(this).updateFlags(flags);
-    }
-
-    @Override
-    public void requestContinueDownload() {
-        if (mControl == CONTROL_PAUSED) {
-            mControl = CONTROL_RUN;
-        }
-        Intent fileIntent = new Intent(this, this.getClass());
-        fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
-        this.startService(fileIntent);
-    }
-
-    public abstract String getPublicKey();
-
-    public abstract byte[] getSALT();
-
-    public abstract String getAlarmReceiverClassName();
-
-    private class LVLRunnable implements Runnable {
-        LVLRunnable(Context context, PendingIntent intent) {
-            mContext = context;
-            mPendingIntent = intent;
-        }
-
-        final Context mContext;
-
-        @Override
-        public void run() {
-            setServiceRunning(true);
-            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL);
-            String deviceId = Secure.getString(mContext.getContentResolver(),
-                    Secure.ANDROID_ID);
-
-            final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
-                    new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId));
-
-            // reset our policy back to the start of the world to force a
-            // re-check
-            aep.resetPolicy();
-
-            // let's try and get the OBB file from LVL first
-            // Construct the LicenseChecker with a Policy.
-            final LicenseChecker checker = new LicenseChecker(mContext, aep,
-                    getPublicKey() // Your public licensing key.
-            );
-            checker.checkAccess(new LicenseCheckerCallback() {
-
-                @Override
-                public void allow(int reason) {
-                    try {
-                        int count = aep.getExpansionURLCount();
-                        DownloadsDB db = DownloadsDB.getDB(mContext);
-                        int status = 0;
-                        if (count != 0) {
-                            for (int i = 0; i < count; i++) {
-                                String currentFileName = aep
-                                        .getExpansionFileName(i);
-                                if (null != currentFileName) {
-                                    DownloadInfo di = new DownloadInfo(i,
-                                            currentFileName, mContext.getPackageName());
-
-                                    long fileSize = aep.getExpansionFileSize(i);
-                                    if (handleFileUpdated(db, i, currentFileName,
-                                            fileSize)) {
-                                        status |= -1;
-                                        di.resetDownload();
-                                        di.mUri = aep.getExpansionURL(i);
-                                        di.mTotalBytes = fileSize;
-                                        di.mStatus = status;
-                                        db.updateDownload(di);
-                                    } else {
-                                        // we need to read the download
-                                        // information
-                                        // from
-                                        // the database
-                                        DownloadInfo dbdi = db
-                                                .getDownloadInfoByFileName(di.mFileName);
-                                        if (null == dbdi) {
-                                            // the file exists already and is
-                                            // the
-                                            // correct size
-                                            // was delivered by Market or
-                                            // through
-                                            // another mechanism
-                                            Log.d(LOG_TAG, "file " + di.mFileName
-                                                    + " found. Not downloading.");
-                                            di.mStatus = STATUS_SUCCESS;
-                                            di.mTotalBytes = fileSize;
-                                            di.mCurrentBytes = fileSize;
-                                            di.mUri = aep.getExpansionURL(i);
-                                            db.updateDownload(di);
-                                        } else if (dbdi.mStatus != STATUS_SUCCESS) {
-                                            // we just update the URL
-                                            dbdi.mUri = aep.getExpansionURL(i);
-                                            db.updateDownload(dbdi);
-                                            status |= -1;
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                        // first: do we need to do an LVL update?
-                        // we begin by getting our APK version from the package
-                        // manager
-                        PackageInfo pi;
-                        try {
-                            pi = mContext.getPackageManager().getPackageInfo(
-                                    mContext.getPackageName(), 0);
-                            db.updateMetadata(pi.versionCode, status);
-                            Class<?> serviceClass = DownloaderService.this.getClass();
-                            switch (startDownloadServiceIfRequired(mContext, mPendingIntent,
-                                    serviceClass)) {
-                                case NO_DOWNLOAD_REQUIRED:
-                                    mNotification
-                                            .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
-                                    break;
-                                case LVL_CHECK_REQUIRED:
-                                    // DANGER WILL ROBINSON!
-                                    Log.e(LOG_TAG, "In LVL checking loop!");
-                                    mNotification
-                                            .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
-                                    throw new RuntimeException(
-                                            "Error with LVL checking and database integrity");
-                                case DOWNLOAD_REQUIRED:
-                                    // do nothing. the download will notify the
-                                    // application
-                                    // when things are done
-                                    break;
-                            }
-                        } catch (NameNotFoundException e1) {
-                            e1.printStackTrace();
-                            throw new RuntimeException(
-                                    "Error with getting information from package name");
-                        }
-                    } finally {
-                        setServiceRunning(false);
-                    }
-                }
-
-                @Override
-                public void dontAllow(int reason) {
-                    try
-                    {
-                        switch (reason) {
-                            case Policy.NOT_LICENSED:
-                                mNotification
-                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
-                                break;
-                            case Policy.RETRY:
-                                mNotification
-                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
-                                break;
-                        }
-                    } finally {
-                        setServiceRunning(false);
-                    }
-
-                }
-
-                @Override
-                public void applicationError(int errorCode) {
-                    try {
-                        mNotification
-                                .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
-                    } finally {
-                        setServiceRunning(false);
-                    }
-                }
-
-            });
-
-        }
-
-    };
-
-    /**
+	public static int startDownloadServiceIfRequired(Context context,
+			PendingIntent pendingIntent, String classPackage, String className)
+			throws NameNotFoundException {
+		// first: do we need to do an LVL update?
+		// we begin by getting our APK version from the package manager
+		final PackageInfo pi = context.getPackageManager().getPackageInfo(
+				context.getPackageName(), 0);
+
+		int status = NO_DOWNLOAD_REQUIRED;
+
+		// the database automatically reads the metadata for version code
+		// and download status when the instance is created
+		DownloadsDB db = DownloadsDB.getDB(context);
+
+		// we need to update the LVL check and get a successful status to
+		// proceed
+		if (isLVLCheckRequired(db, pi)) {
+			status = LVL_CHECK_REQUIRED;
+		}
+		// we don't have to update LVL. do we still have a download to start?
+		if (db.mStatus == 0) {
+			DownloadInfo[] infos = db.getDownloads();
+			if (null != infos) {
+				for (DownloadInfo info : infos) {
+					if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) {
+						status = DOWNLOAD_REQUIRED;
+						db.updateStatus(-1);
+						break;
+					}
+				}
+			}
+		} else {
+			status = DOWNLOAD_REQUIRED;
+		}
+		switch (status) {
+			case DOWNLOAD_REQUIRED:
+			case LVL_CHECK_REQUIRED:
+				Intent fileIntent = new Intent();
+				fileIntent.setClassName(classPackage, className);
+				fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
+				context.startService(fileIntent);
+				break;
+		}
+		return status;
+	}
+
+	@Override
+	public void requestAbortDownload() {
+		mControl = CONTROL_PAUSED;
+		mStatus = STATUS_CANCELED;
+	}
+
+	@Override
+	public void requestPauseDownload() {
+		mControl = CONTROL_PAUSED;
+		mStatus = STATUS_PAUSED_BY_APP;
+	}
+
+	@Override
+	public void setDownloadFlags(int flags) {
+		DownloadsDB.getDB(this).updateFlags(flags);
+	}
+
+	@Override
+	public void requestContinueDownload() {
+		if (mControl == CONTROL_PAUSED) {
+			mControl = CONTROL_RUN;
+		}
+		Intent fileIntent = new Intent(this, this.getClass());
+		fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+		this.startService(fileIntent);
+	}
+
+	public abstract String getPublicKey();
+
+	public abstract byte[] getSALT();
+
+	public abstract String getAlarmReceiverClassName();
+
+	private class LVLRunnable implements Runnable {
+		LVLRunnable(Context context, PendingIntent intent) {
+			mContext = context;
+			mPendingIntent = intent;
+		}
+
+		final Context mContext;
+
+		@Override
+		public void run() {
+			setServiceRunning(true);
+			mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL);
+			String deviceId = Secure.ANDROID_ID;
+
+			final APKExpansionPolicy aep = new APKExpansionPolicy(mContext,
+					new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId));
+
+			// reset our policy back to the start of the world to force a
+			// re-check
+			aep.resetPolicy();
+
+			// let's try and get the OBB file from LVL first
+			// Construct the LicenseChecker with a Policy.
+			final LicenseChecker checker = new LicenseChecker(mContext, aep,
+					getPublicKey() // Your public licensing key.
+			);
+			checker.checkAccess(new LicenseCheckerCallback() {
+				@Override
+				public void allow(int reason) {
+					try {
+						int count = aep.getExpansionURLCount();
+						DownloadsDB db = DownloadsDB.getDB(mContext);
+						int status = 0;
+						if (count != 0) {
+							for (int i = 0; i < count; i++) {
+								String currentFileName = aep
+																 .getExpansionFileName(i);
+								if (null != currentFileName) {
+									DownloadInfo di = new DownloadInfo(i,
+											currentFileName, mContext.getPackageName());
+
+									long fileSize = aep.getExpansionFileSize(i);
+									if (handleFileUpdated(db, i, currentFileName,
+												fileSize)) {
+										status |= -1;
+										di.resetDownload();
+										di.mUri = aep.getExpansionURL(i);
+										di.mTotalBytes = fileSize;
+										di.mStatus = status;
+										db.updateDownload(di);
+									} else {
+										// we need to read the download
+										// information
+										// from
+										// the database
+										DownloadInfo dbdi = db
+																	.getDownloadInfoByFileName(di.mFileName);
+										if (null == dbdi) {
+											// the file exists already and is
+											// the
+											// correct size
+											// was delivered by Market or
+											// through
+											// another mechanism
+											Log.d(LOG_TAG, "file " + di.mFileName + " found. Not downloading.");
+											di.mStatus = STATUS_SUCCESS;
+											di.mTotalBytes = fileSize;
+											di.mCurrentBytes = fileSize;
+											di.mUri = aep.getExpansionURL(i);
+											db.updateDownload(di);
+										} else if (dbdi.mStatus != STATUS_SUCCESS) {
+											// we just update the URL
+											dbdi.mUri = aep.getExpansionURL(i);
+											db.updateDownload(dbdi);
+											status |= -1;
+										}
+									}
+								}
+							}
+						}
+						// first: do we need to do an LVL update?
+						// we begin by getting our APK version from the package
+						// manager
+						PackageInfo pi;
+						try {
+							pi = mContext.getPackageManager().getPackageInfo(
+									mContext.getPackageName(), 0);
+							db.updateMetadata(pi.versionCode, status);
+							Class<?> serviceClass = DownloaderService.this.getClass();
+							switch (startDownloadServiceIfRequired(mContext, mPendingIntent,
+									serviceClass)) {
+								case NO_DOWNLOAD_REQUIRED:
+									mNotification
+											.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
+									break;
+								case LVL_CHECK_REQUIRED:
+									// DANGER WILL ROBINSON!
+									Log.e(LOG_TAG, "In LVL checking loop!");
+									mNotification
+											.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
+									throw new RuntimeException(
+											"Error with LVL checking and database integrity");
+								case DOWNLOAD_REQUIRED:
+									// do nothing. the download will notify the
+									// application
+									// when things are done
+									break;
+							}
+						} catch (NameNotFoundException e1) {
+							e1.printStackTrace();
+							throw new RuntimeException(
+									"Error with getting information from package name");
+						}
+					} finally {
+						setServiceRunning(false);
+					}
+				}
+
+				@Override
+				public void dontAllow(int reason) {
+					try {
+						switch (reason) {
+							case Policy.NOT_LICENSED:
+								mNotification
+										.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED);
+								break;
+							case Policy.RETRY:
+								mNotification
+										.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
+								break;
+						}
+					} finally {
+						setServiceRunning(false);
+					}
+				}
+
+				@Override
+				public void applicationError(int errorCode) {
+					try {
+						mNotification
+								.onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL);
+					} finally {
+						setServiceRunning(false);
+					}
+				}
+			});
+		}
+	};
+
+	/**
      * Updates the LVL information from the server.
-     * 
+     *
      * @param context
      */
-    public void updateLVL(final Context context) {
-        Context c = context.getApplicationContext();
-        Handler h = new Handler(c.getMainLooper());
-        h.post(new LVLRunnable(c, mPendingIntent));
-    }
+	public void updateLVL(final Context context) {
+		Context c = context.getApplicationContext();
+		Handler h = new Handler(c.getMainLooper());
+		h.post(new LVLRunnable(c, mPendingIntent));
+	}
 
-    /**
+	/**
      * The APK has been updated and a filename has been sent down from the
      * Market call. If the file has the same name as the previous file, we do
      * nothing as the file is guaranteed to be the same. If the file does not
      * have the same name, we download it if it hasn't already been delivered by
      * Market.
-     * 
+     *
      * @param index the index of the file from market (0 = main, 1 = patch)
      * @param filename the name of the new file
      * @param fileSize the size of the new file
      * @return
      */
-    public boolean handleFileUpdated(DownloadsDB db, int index,
-            String filename, long fileSize) {
-        DownloadInfo di = db.getDownloadInfoByFileName(filename);
-        if (null != di) {
-            String oldFile = di.mFileName;
-            // cleanup
-            if (null != oldFile) {
-                if (filename.equals(oldFile)) {
-                    return false;
-                }
-
-                // remove partially downloaded file if it is there
-                String deleteFile = Helpers.generateSaveFileName(this, oldFile);
-                File f = new File(deleteFile);
-                if (f.exists())
-                    f.delete();
-            }
-        }
-        return !Helpers.doesFileExist(this, filename, fileSize, true);
-    }
-
-    private void scheduleAlarm(long wakeUp) {
-        AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-        if (alarms == null) {
-            Log.e(Constants.TAG, "couldn't get alarm manager");
-            return;
-        }
-
-        if (Constants.LOGV) {
-            Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
-        }
-
-        String className = getAlarmReceiverClassName();
-        Intent intent = new Intent(Constants.ACTION_RETRY);
-        intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
-        intent.setClassName(this.getPackageName(),
-                className);
-        mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent,
-                PendingIntent.FLAG_ONE_SHOT);
-        alarms.set(
-                AlarmManager.RTC_WAKEUP,
-                System.currentTimeMillis() + wakeUp, mAlarmIntent
-                );
-    }
-
-    private void cancelAlarms() {
-        if (null != mAlarmIntent) {
-            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-            if (alarms == null) {
-                Log.e(Constants.TAG, "couldn't get alarm manager");
-                return;
-            }
-            alarms.cancel(mAlarmIntent);
-            mAlarmIntent = null;
-        }
-    }
-
-    /**
+	public boolean handleFileUpdated(DownloadsDB db, int index,
+			String filename, long fileSize) {
+		DownloadInfo di = db.getDownloadInfoByFileName(filename);
+		if (null != di) {
+			String oldFile = di.mFileName;
+			// cleanup
+			if (null != oldFile) {
+				if (filename.equals(oldFile)) {
+					return false;
+				}
+
+				// remove partially downloaded file if it is there
+				String deleteFile = Helpers.generateSaveFileName(this, oldFile);
+				File f = new File(deleteFile);
+				if (f.exists())
+					f.delete();
+			}
+		}
+		return !Helpers.doesFileExist(this, filename, fileSize, true);
+	}
+
+	private void scheduleAlarm(long wakeUp) {
+		AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+		if (alarms == null) {
+			Log.e(Constants.TAG, "couldn't get alarm manager");
+			return;
+		}
+
+		if (Constants.LOGV) {
+			Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+		}
+
+		String className = getAlarmReceiverClassName();
+		Intent intent = new Intent(Constants.ACTION_RETRY);
+		intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+		intent.setClassName(this.getPackageName(),
+				className);
+		mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent,
+				PendingIntent.FLAG_ONE_SHOT);
+		alarms.set(
+				AlarmManager.RTC_WAKEUP,
+				System.currentTimeMillis() + wakeUp, mAlarmIntent);
+	}
+
+	private void cancelAlarms() {
+		if (null != mAlarmIntent) {
+			AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+			if (alarms == null) {
+				Log.e(Constants.TAG, "couldn't get alarm manager");
+				return;
+			}
+			alarms.cancel(mAlarmIntent);
+			mAlarmIntent = null;
+		}
+	}
+
+	/**
      * We use this to track network state, such as when WiFi, Cellular, etc. is
      * enabled when downloads are paused or in progress.
      */
-    private class InnerBroadcastReceiver extends BroadcastReceiver {
-        final Service mService;
-
-        InnerBroadcastReceiver(Service service) {
-            mService = service;
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            pollNetworkState();
-            if (mStateChanged
-                    && !isServiceRunning()) {
-                Log.d(Constants.TAG, "InnerBroadcastReceiver Called");
-                Intent fileIntent = new Intent(context, mService.getClass());
-                fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
-                // send a new intent to the service
-                context.startService(fileIntent);
-            }
-        }
-    };
-
-    /**
+	private class InnerBroadcastReceiver extends BroadcastReceiver {
+		final Service mService;
+
+		InnerBroadcastReceiver(Service service) {
+			mService = service;
+		}
+
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			pollNetworkState();
+			if (mStateChanged && !isServiceRunning()) {
+				Log.d(Constants.TAG, "InnerBroadcastReceiver Called");
+				Intent fileIntent = new Intent(context, mService.getClass());
+				fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent);
+				// send a new intent to the service
+				context.startService(fileIntent);
+			}
+		}
+	};
+
+	/**
      * This is the main thread for the Downloader. This thread is responsible
      * for queuing up downloads and other goodness.
      */
-    @Override
-    protected void onHandleIntent(Intent intent) {
-        setServiceRunning(true);
-        try {
-            // the database automatically reads the metadata for version code
-            // and download status when the instance is created
-            DownloadsDB db = DownloadsDB.getDB(this);
-            final PendingIntent pendingIntent = (PendingIntent) intent
-                    .getParcelableExtra(EXTRA_PENDING_INTENT);
-
-            if (null != pendingIntent)
-            {
-                mNotification.setClientIntent(pendingIntent);
-                mPendingIntent = pendingIntent;
-            } else if (null != mPendingIntent) {
-                mNotification.setClientIntent(mPendingIntent);
-            } else {
-                Log.e(LOG_TAG, "Downloader started in bad state without notification intent.");
-                return;
-            }
-
-            // when the LVL check completes, a successful response will update
-            // the service
-            if (isLVLCheckRequired(db, mPackageInfo)) {
-                updateLVL(this);
-                return;
-            }
-
-            // get each download
-            DownloadInfo[] infos = db.getDownloads();
-            mBytesSoFar = 0;
-            mTotalLength = 0;
-            mFileCount = infos.length;
-            for (DownloadInfo info : infos) {
-                // We do an (simple) integrity check on each file, just to make
-                // sure
-                if (info.mStatus == STATUS_SUCCESS) {
-                    // verify that the file matches the state
-                    if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) {
-                        info.mStatus = 0;
-                        info.mCurrentBytes = 0;
-                    }
-                }
-                // get aggregate data
-                mTotalLength += info.mTotalBytes;
-                mBytesSoFar += info.mCurrentBytes;
-            }
-
-            // loop through all downloads and fetch them
-            pollNetworkState();
-            if (null == mConnReceiver) {
-
-                /**
+	@Override
+	protected void onHandleIntent(Intent intent) {
+		setServiceRunning(true);
+		try {
+			// the database automatically reads the metadata for version code
+			// and download status when the instance is created
+			DownloadsDB db = DownloadsDB.getDB(this);
+			final PendingIntent pendingIntent = (PendingIntent)intent
+														.getParcelableExtra(EXTRA_PENDING_INTENT);
+
+			if (null != pendingIntent) {
+				mNotification.setClientIntent(pendingIntent);
+				mPendingIntent = pendingIntent;
+			} else if (null != mPendingIntent) {
+				mNotification.setClientIntent(mPendingIntent);
+			} else {
+				Log.e(LOG_TAG, "Downloader started in bad state without notification intent.");
+				return;
+			}
+
+			// when the LVL check completes, a successful response will update
+			// the service
+			if (isLVLCheckRequired(db, mPackageInfo)) {
+				updateLVL(this);
+				return;
+			}
+
+			// get each download
+			DownloadInfo[] infos = db.getDownloads();
+			mBytesSoFar = 0;
+			mTotalLength = 0;
+			mFileCount = infos.length;
+			for (DownloadInfo info : infos) {
+				// We do an (simple) integrity check on each file, just to make
+				// sure
+				if (info.mStatus == STATUS_SUCCESS) {
+					// verify that the file matches the state
+					if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) {
+						info.mStatus = 0;
+						info.mCurrentBytes = 0;
+					}
+				}
+				// get aggregate data
+				mTotalLength += info.mTotalBytes;
+				mBytesSoFar += info.mCurrentBytes;
+			}
+
+			// loop through all downloads and fetch them
+			pollNetworkState();
+			if (null == mConnReceiver) {
+
+				/**
                  * We use this to track network state, such as when WiFi,
                  * Cellular, etc. is enabled when downloads are paused or in
                  * progress.
                  */
-                mConnReceiver = new InnerBroadcastReceiver(this);
-                IntentFilter intentFilter = new IntentFilter(
-                        ConnectivityManager.CONNECTIVITY_ACTION);
-                intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
-                registerReceiver(mConnReceiver, intentFilter);
-            }
-
-            for (DownloadInfo info : infos) {
-                long startingCount = info.mCurrentBytes;
-
-                if (info.mStatus != STATUS_SUCCESS) {
-                    DownloadThread dt = new DownloadThread(info, this, mNotification);
-                    cancelAlarms();
-                    scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG);
-                    dt.run();
-                    cancelAlarms();
-                }
-                db.updateFromDb(info);
-                boolean setWakeWatchdog = false;
-                int notifyStatus;
-                switch (info.mStatus) {
-                    case STATUS_FORBIDDEN:
-                        // the URL is out of date
-                        updateLVL(this);
-                        return;
-                    case STATUS_SUCCESS:
-                        mBytesSoFar += info.mCurrentBytes - startingCount;
-                        db.updateMetadata(mPackageInfo.versionCode, 0);
-                        continue;
-                    case STATUS_FILE_DELIVERED_INCORRECTLY:
-                        // we may be on a network that is returning us a web
-                        // page on redirect
-                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE;
-                        info.mCurrentBytes = 0;
-                        db.updateDownload(info);
-                        setWakeWatchdog = true;
-                        break;
-                    case STATUS_PAUSED_BY_APP:
-                        notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST;
-                        break;
-                    case STATUS_WAITING_FOR_NETWORK:
-                    case STATUS_WAITING_TO_RETRY:
-                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE;
-                        setWakeWatchdog = true;
-                        break;
-                    case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION:
-                    case STATUS_QUEUED_FOR_WIFI:
-                        // look for more detail here
-                        if (null != mWifiManager) {
-                            if (!mWifiManager.isWifiEnabled()) {
-                                notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION;
-                                setWakeWatchdog = true;
-                                break;
-                            }
-                        }
-                        notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION;
-                        setWakeWatchdog = true;
-                        break;
-                    case STATUS_CANCELED:
-                        notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED;
-                        setWakeWatchdog = true;
-                        break;
-
-                    case STATUS_INSUFFICIENT_SPACE_ERROR:
-                        notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL;
-                        setWakeWatchdog = true;
-                        break;
-
-                    case STATUS_DEVICE_NOT_FOUND_ERROR:
-                        notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE;
-                        setWakeWatchdog = true;
-                        break;
-
-                    default:
-                        notifyStatus = IDownloaderClient.STATE_FAILED;
-                        break;
-                }
-                if (setWakeWatchdog) {
-                    scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER);
-                } else {
-                    cancelAlarms();
-                }
-                // failure or pause state
-                mNotification.onDownloadStateChanged(notifyStatus);
-                return;
-            }
-
-            // all downloads complete
-            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
-        } finally {
-            setServiceRunning(false);
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        if (null != mConnReceiver) {
-            unregisterReceiver(mConnReceiver);
-            mConnReceiver = null;
-        }
-        mServiceStub.disconnect(this);
-        super.onDestroy();
-    }
-
-    public int getNetworkAvailabilityState(DownloadsDB db) {
-        if (mIsConnected) {
-            if (!mIsCellularConnection)
-                return NETWORK_OK;
-            int flags = db.mFlags;
-            if (mIsRoaming)
-                return NETWORK_CANNOT_USE_ROAMING;
-            if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
-                return NETWORK_OK;
-            } else {
-                return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
-            }
-        }
-        return NETWORK_NO_CONNECTION;
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        try {
-            mPackageInfo = getPackageManager().getPackageInfo(
-                    getPackageName(), 0);
-            ApplicationInfo ai = getApplicationInfo();
-            CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai);
-            mNotification = new DownloadNotification(this, applicationLabel);
-
-        } catch (NameNotFoundException e) {
-            e.printStackTrace();
-        }
-    }
-
-    /**
+				mConnReceiver = new InnerBroadcastReceiver(this);
+				IntentFilter intentFilter = new IntentFilter(
+						ConnectivityManager.CONNECTIVITY_ACTION);
+				intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+				registerReceiver(mConnReceiver, intentFilter);
+			}
+
+			for (DownloadInfo info : infos) {
+				long startingCount = info.mCurrentBytes;
+
+				if (info.mStatus != STATUS_SUCCESS) {
+					DownloadThread dt = new DownloadThread(info, this, mNotification);
+					cancelAlarms();
+					scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG);
+					dt.run();
+					cancelAlarms();
+				}
+				db.updateFromDb(info);
+				boolean setWakeWatchdog = false;
+				int notifyStatus;
+				switch (info.mStatus) {
+					case STATUS_FORBIDDEN:
+						// the URL is out of date
+						updateLVL(this);
+						return;
+					case STATUS_SUCCESS:
+						mBytesSoFar += info.mCurrentBytes - startingCount;
+						db.updateMetadata(mPackageInfo.versionCode, 0);
+						continue;
+					case STATUS_FILE_DELIVERED_INCORRECTLY:
+						// we may be on a network that is returning us a web
+						// page on redirect
+						notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE;
+						info.mCurrentBytes = 0;
+						db.updateDownload(info);
+						setWakeWatchdog = true;
+						break;
+					case STATUS_PAUSED_BY_APP:
+						notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST;
+						break;
+					case STATUS_WAITING_FOR_NETWORK:
+					case STATUS_WAITING_TO_RETRY:
+						notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE;
+						setWakeWatchdog = true;
+						break;
+					case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION:
+					case STATUS_QUEUED_FOR_WIFI:
+						// look for more detail here
+						if (null != mWifiManager) {
+							if (!mWifiManager.isWifiEnabled()) {
+								notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION;
+								setWakeWatchdog = true;
+								break;
+							}
+						}
+						notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION;
+						setWakeWatchdog = true;
+						break;
+					case STATUS_CANCELED:
+						notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED;
+						setWakeWatchdog = true;
+						break;
+
+					case STATUS_INSUFFICIENT_SPACE_ERROR:
+						notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL;
+						setWakeWatchdog = true;
+						break;
+
+					case STATUS_DEVICE_NOT_FOUND_ERROR:
+						notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE;
+						setWakeWatchdog = true;
+						break;
+
+					default:
+						notifyStatus = IDownloaderClient.STATE_FAILED;
+						break;
+				}
+				if (setWakeWatchdog) {
+					scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER);
+				} else {
+					cancelAlarms();
+				}
+				// failure or pause state
+				mNotification.onDownloadStateChanged(notifyStatus);
+				return;
+			}
+
+			// all downloads complete
+			mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED);
+		} finally {
+			setServiceRunning(false);
+		}
+	}
+
+	@Override
+	public void onDestroy() {
+		if (null != mConnReceiver) {
+			unregisterReceiver(mConnReceiver);
+			mConnReceiver = null;
+		}
+		mServiceStub.disconnect(this);
+		super.onDestroy();
+	}
+
+	public int getNetworkAvailabilityState(DownloadsDB db) {
+		if (mIsConnected) {
+			if (!mIsCellularConnection)
+				return NETWORK_OK;
+			int flags = db.mFlags;
+			if (mIsRoaming)
+				return NETWORK_CANNOT_USE_ROAMING;
+			if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) {
+				return NETWORK_OK;
+			} else {
+				return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+			}
+		}
+		return NETWORK_NO_CONNECTION;
+	}
+
+	@Override
+	public void onCreate() {
+		super.onCreate();
+		try {
+			mPackageInfo = getPackageManager().getPackageInfo(
+					getPackageName(), 0);
+			ApplicationInfo ai = getApplicationInfo();
+			CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai);
+			mNotification = new DownloadNotification(this, applicationLabel);
+
+		} catch (NameNotFoundException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
      * Exception thrown from methods called by generateSaveFile() for any fatal
      * error.
      */
-    public static class GenerateSaveFileError extends Exception {
-        private static final long serialVersionUID = 3465966015408936540L;
-        int mStatus;
-        String mMessage;
+	public static class GenerateSaveFileError extends Exception {
+		private static final long serialVersionUID = 3465966015408936540L;
+		int mStatus;
+		String mMessage;
 
-        public GenerateSaveFileError(int status, String message) {
-            mStatus = status;
-            mMessage = message;
-        }
-    }
+		public GenerateSaveFileError(int status, String message) {
+			mStatus = status;
+			mMessage = message;
+		}
+	}
 
-    /**
+	/**
      * Returns the filename (where the file should be saved) from info about a
      * download
      */
-    public String generateTempSaveFileName(String fileName) {
-        String path = Helpers.getSaveFilePath(this)
-                + File.separator + fileName + TEMP_EXT;
-        return path;
-    }
+	public String generateTempSaveFileName(String fileName) {
+		String path = Helpers.getSaveFilePath(this) + File.separator + fileName + TEMP_EXT;
+		return path;
+	}
 
-    /**
+	/**
      * Creates a filename (where the file should be saved) from info about a
      * download.
      */
-    public String generateSaveFile(String filename, long filesize)
-            throws GenerateSaveFileError {
-        String path = generateTempSaveFileName(filename);
-        File expPath = new File(path);
-        if (!Helpers.isExternalMediaMounted()) {
-            Log.d(Constants.TAG, "External media not mounted: " + path);
-            throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR,
-                    "external media is not yet mounted");
-
-        }
-        if (expPath.exists()) {
-            Log.d(Constants.TAG, "File already exists: " + path);
-            throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR,
-                    "requested destination file already exists");
-        }
-        if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) {
-            throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR,
-                    "insufficient space on external storage");
-        }
-        return path;
-    }
-
-    /**
+	public String generateSaveFile(String filename, long filesize)
+			throws GenerateSaveFileError {
+		String path = generateTempSaveFileName(filename);
+		File expPath = new File(path);
+		if (!Helpers.isExternalMediaMounted()) {
+			Log.d(Constants.TAG, "External media not mounted: " + path);
+			throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR,
+					"external media is not yet mounted");
+		}
+		if (expPath.exists()) {
+			Log.d(Constants.TAG, "File already exists: " + path);
+			throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR,
+					"requested destination file already exists");
+		}
+		if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) {
+			throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR,
+					"insufficient space on external storage");
+		}
+		return path;
+	}
+
+	/**
      * @return a non-localized string appropriate for logging corresponding to
      *         one of the NETWORK_* constants.
      */
-    public String getLogMessageForNetworkError(int networkError) {
-        switch (networkError) {
-            case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
-                return "download size exceeds recommended limit for mobile network";
+	public String getLogMessageForNetworkError(int networkError) {
+		switch (networkError) {
+			case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
+				return "download size exceeds recommended limit for mobile network";
 
-            case NETWORK_UNUSABLE_DUE_TO_SIZE:
-                return "download size exceeds limit for mobile network";
+			case NETWORK_UNUSABLE_DUE_TO_SIZE:
+				return "download size exceeds limit for mobile network";
 
-            case NETWORK_NO_CONNECTION:
-                return "no network connection available";
+			case NETWORK_NO_CONNECTION:
+				return "no network connection available";
 
-            case NETWORK_CANNOT_USE_ROAMING:
-                return "download cannot use the current network connection because it is roaming";
+			case NETWORK_CANNOT_USE_ROAMING:
+				return "download cannot use the current network connection because it is roaming";
 
-            case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
-                return "download was requested to not use the current network type";
+			case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
+				return "download was requested to not use the current network type";
 
-            default:
-                return "unknown error with network connectivity";
-        }
-    }
+			default:
+				return "unknown error with network connectivity";
+		}
+	}
 
-    public int getControl() {
-        return mControl;
-    }
+	public int getControl() {
+		return mControl;
+	}
 
-    public int getStatus() {
-        return mStatus;
-    }
+	public int getStatus() {
+		return mStatus;
+	}
 
-    /**
+	/**
      * Calculating a moving average for the speed so we don't get jumpy
      * calculations for time etc.
      */
-    static private final float SMOOTHING_FACTOR = 0.005f;
-
-    public void notifyUpdateBytes(long totalBytesSoFar) {
-        long timeRemaining;
-        long currentTime = SystemClock.uptimeMillis();
-        if (0 != mMillisecondsAtSample) {
-            // we have a sample.
-            long timePassed = currentTime - mMillisecondsAtSample;
-            long bytesInSample = totalBytesSoFar - mBytesAtSample;
-            float currentSpeedSample = (float) bytesInSample / (float) timePassed;
-            if (0 != mAverageDownloadSpeed) {
-                mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample
-                        + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed;
-            } else {
-                mAverageDownloadSpeed = currentSpeedSample;
-            }
-            timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed);
-        } else {
-            timeRemaining = -1;
-        }
-        mMillisecondsAtSample = currentTime;
-        mBytesAtSample = totalBytesSoFar;
-        mNotification.onDownloadProgress(
-                new DownloadProgressInfo(mTotalLength,
-                        totalBytesSoFar,
-                        timeRemaining,
-                        mAverageDownloadSpeed)
-                );
-
-    }
-
-    @Override
-    protected boolean shouldStop() {
-        // the database automatically reads the metadata for version code
-        // and download status when the instance is created
-        DownloadsDB db = DownloadsDB.getDB(this);
-        if (db.mStatus == 0) {
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public void requestDownloadStatus() {
-        mNotification.resendState();
-    }
-
-    @Override
-    public void onClientUpdated(Messenger clientMessenger) {
-        this.mClientMessenger = clientMessenger;
-        mNotification.setMessenger(mClientMessenger);
-    }
-
+	static private final float SMOOTHING_FACTOR = 0.005f;
+
+	public void notifyUpdateBytes(long totalBytesSoFar) {
+		long timeRemaining;
+		long currentTime = SystemClock.uptimeMillis();
+		if (0 != mMillisecondsAtSample) {
+			// we have a sample.
+			long timePassed = currentTime - mMillisecondsAtSample;
+			long bytesInSample = totalBytesSoFar - mBytesAtSample;
+			float currentSpeedSample = (float)bytesInSample / (float)timePassed;
+			if (0 != mAverageDownloadSpeed) {
+				mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed;
+			} else {
+				mAverageDownloadSpeed = currentSpeedSample;
+			}
+			timeRemaining = (long)((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed);
+		} else {
+			timeRemaining = -1;
+		}
+		mMillisecondsAtSample = currentTime;
+		mBytesAtSample = totalBytesSoFar;
+		mNotification.onDownloadProgress(
+				new DownloadProgressInfo(mTotalLength,
+						totalBytesSoFar,
+						timeRemaining,
+						mAverageDownloadSpeed));
+	}
+
+	@Override
+	protected boolean shouldStop() {
+		// the database automatically reads the metadata for version code
+		// and download status when the instance is created
+		DownloadsDB db = DownloadsDB.getDB(this);
+		if (db.mStatus == 0) {
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public void requestDownloadStatus() {
+		mNotification.resendState();
+	}
+
+	@Override
+	public void onClientUpdated(Messenger clientMessenger) {
+		this.mClientMessenger = clientMessenger;
+		mNotification.setMessenger(mClientMessenger);
+	}
 }

+ 423 - 464
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java

@@ -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;
-
-    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 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;
-    }
-
-    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 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 close() {
-        this.mHelper.close();
-    }
-
-    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 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;
+	}
+
+	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 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;
+	}
+
+	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 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();
+	}
+
+	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(',');
+			}
+		}
+
+		/**
          * 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 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 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 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[][] 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 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;
-
-    /**
+		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 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 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 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";
+	}
+
+	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;
+
+	/**
      * 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) {
-        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 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 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 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 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 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 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 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 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 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(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 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();
+			}
+		}
+	}
 }

+ 143 - 152
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java

@@ -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 class TimeOfDay {
-        TimeOfDay(int h, int m, int s) {
-            this.hour = h;
-            this.minute = m;
-            this.second = s;
-        }
-
-        int hour;
-        int minute;
-        int second;
-    }
-
-    public static long parse(String timeString)
-            throws IllegalArgumentException {
-
-        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();
-            }
-        }
-
-        // 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 */);
-    }
-
-    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 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;
+		}
+
+		int hour;
+		int minute;
+		int second;
+	}
+
+	public static long parse(String timeString)
+			throws IllegalArgumentException {
+
+		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();
+			}
+		}
+
+		// 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 */);
+	}
+
+	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 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++;
-
-        int minute = (timeString.charAt(i++) - '0') * 10
-                + (timeString.charAt(i++) - '0');
-        // Skip ':'
-        i++;
-
-        int second = (timeString.charAt(i++) - '0') * 10
-                + (timeString.charAt(i++) - '0');
-
-        return new TimeOfDay(hour, minute, second);
-    }
+	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 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 second = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0');
+
+		return new TimeOfDay(hour, minute, second);
+	}
 }

+ 0 - 101
platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java

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

+ 110 - 0
platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java

@@ -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);
+		}
+	}
+}

+ 413 - 0
platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java

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

+ 2 - 2
platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java → platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java

@@ -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
platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl → platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl


+ 100 - 0
platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java

@@ -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
platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl → platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl


+ 100 - 0
platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java

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

+ 387 - 0
platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java

@@ -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 "";
+		}
+	}
+}

+ 15 - 15
platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java → platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java

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

+ 232 - 0
platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java

@@ -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);
+	}
+}

+ 3 - 3
platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java → platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java

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

+ 8 - 8
platform/android/java/src/com/android/vending/licensing/Obfuscator.java → platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java

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

+ 18 - 12
platform/android/java/src/com/android/vending/licensing/Policy.java → platform/android/java/src/com/google/android/vending/licensing/Policy.java

@@ -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();
 }

+ 78 - 0
platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java

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

+ 80 - 0
platform/android/java/src/com/google/android/vending/licensing/ResponseData.java

@@ -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 });
+	}
+}

+ 299 - 0
platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java

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

+ 51 - 15
platform/android/java/src/com/android/vending/licensing/StrictPolicy.java → platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java

@@ -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";
+
+	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;
-    }
+	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;
+	}
 }

+ 7 - 7
platform/android/java/src/com/android/vending/licensing/ValidationException.java → platform/android/java/src/com/google/android/vending/licensing/ValidationException.java

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

+ 556 - 0
platform/android/java/src/com/google/android/vending/licensing/util/Base64.java

@@ -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 [email protected]
+ * @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;
+	}
+}

+ 7 - 7
platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java → platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java

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

+ 60 - 0
platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java

@@ -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.");
+		}
+	}
+}

+ 67 - 81
platform/android/java/src/org/godotengine/godot/Godot.java

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

+ 19 - 19
platform/android/java/src/org/godotengine/godot/GodotIO.java

@@ -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) {

+ 34 - 25
platform/android/java/src/org/godotengine/godot/GodotView.java

@@ -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) {
 

+ 43 - 29
platform/android/java/src/org/godotengine/godot/input/GodotEditText.java

@@ -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;
+		}
 	}
 
 	// ===========================================================

+ 1 - 6
platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java

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

+ 0 - 209
platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java

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

+ 55 - 33
platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java

@@ -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);

+ 0 - 79
platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java

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

+ 2 - 2
platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java

@@ -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) {

+ 2 - 2
platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java

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

+ 58 - 24
platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java

@@ -30,26 +30,59 @@
 
 package org.godotengine.godot.payments;
 
-import java.util.ArrayList;
-
-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 com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+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();

+ 82 - 47
platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java

@@ -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;
+		mSku = sku;
+		new ValidateAsyncTask(this).execute();
+	}
 
-			@Override
-			protected void onPreExecute() {
-				dialog = ProgressDialog.show(context, null, "Please wait...");
-			}
+	private 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;
-			}
+	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;
+	}
 
-			@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());
-				}
+	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());
 		}
-				.execute();
 	}
+
 	abstract protected void success();
 	abstract protected void error(String message);
 	abstract protected void canceled();

+ 2 - 2
platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java

@@ -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) {