diff options
Diffstat (limited to 'platform/android/java')
79 files changed, 6456 insertions, 7274 deletions
diff --git a/platform/android/java/README.md b/platform/android/java/README.md new file mode 100644 index 0000000000..58d2b10706 --- /dev/null +++ b/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(); +``` diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.jar b/platform/android/java/gradle/wrapper/gradle-wrapper.jar Binary files differindex 13372aef5e..f6b961fd5a 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.jar +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.jar diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index 6fb3a79546..bf3de21830 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/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 diff --git a/platform/android/java/gradlew b/platform/android/java/gradlew index 9d82f78915..cccdd3d517 100755 --- a/platform/android/java/gradlew +++ b/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" "$@" diff --git a/platform/android/java/gradlew.bat b/platform/android/java/gradlew.bat index 8a0b282aa6..e95643d6a2 100644 --- a/platform/android/java/gradlew.bat +++ b/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
 diff --git a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differindex 372b763ec5..2c246b04a4 100644 --- a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png +++ b/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differindex c61c440636..8bcd464bed 100644 --- a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png +++ b/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable/icon.png b/platform/android/java/res/drawable-nodpi/icon.png Binary files differindex 6ad9b43117..6ad9b43117 100644 --- a/platform/android/java/res/drawable/icon.png +++ b/platform/android/java/res/drawable-nodpi/icon.png diff --git a/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..372b763ec5 --- /dev/null +++ b/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..b458ff3057 --- /dev/null +++ b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/layout/downloading_expansion.xml b/platform/android/java/res/layout/downloading_expansion.xml index d678d94eac..4a9700965f 100644 --- a/platform/android/java/res/layout/downloading_expansion.xml +++ b/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> diff --git a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml index 104993da7e..fae1faeb60 100644 --- a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml +++ b/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> diff --git a/platform/android/java/res/values-ko/strings.xml b/platform/android/java/res/values-ko/strings.xml index b997b934b2..fab0bdd753 100644 --- a/platform/android/java/res/values-ko/strings.xml +++ b/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> diff --git a/platform/android/java/res/values-v11/styles.xml b/platform/android/java/res/values-v11/styles.xml deleted file mode 100644 index f2013bc0bf..0000000000 --- a/platform/android/java/res/values-v11/styles.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/platform/android/java/res/values-v9/styles.xml b/platform/android/java/res/values-v9/styles.xml deleted file mode 100644 index 736e77a5d6..0000000000 --- a/platform/android/java/res/values-v9/styles.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/platform/android/java/res/values/strings.xml b/platform/android/java/res/values/strings.xml index f0ea56148f..a1b81a6186 100644 --- a/platform/android/java/res/values/strings.xml +++ b/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> diff --git a/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java b/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java deleted file mode 100644 index ee12c68deb..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/AESObfuscator.java +++ /dev/null @@ -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); -        } -    } -} diff --git a/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java deleted file mode 100644 index 17cc7a7cfd..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java +++ /dev/null @@ -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; -    } - -} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java b/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java deleted file mode 100644 index 63720999a7..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java +++ /dev/null @@ -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; -} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicensingService.java b/platform/android/java/src/com/android/vending/licensing/ILicensingService.java deleted file mode 100644 index 36afc0537d..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ILicensingService.java +++ /dev/null @@ -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; -} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java b/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java deleted file mode 100644 index 531cb22f8c..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/LicenseChecker.java +++ /dev/null @@ -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 ""; -        } -    } -} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java b/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java deleted file mode 100644 index 61d3c7e79e..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/LicenseValidator.java +++ /dev/null @@ -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); -    } -} diff --git a/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java deleted file mode 100644 index 7c42bfc28a..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java +++ /dev/null @@ -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; -        } -    } -} diff --git a/platform/android/java/src/com/android/vending/licensing/ResponseData.java b/platform/android/java/src/com/android/vending/licensing/ResponseData.java deleted file mode 100644 index 2adef3709e..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ResponseData.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - *      http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.vending.licensing; - -import java.util.regex.Pattern; - -import android.text.TextUtils; - -/** - * ResponseData from licensing server. - */ -public class ResponseData { - -    public int responseCode; -    public int nonce; -    public String packageName; -    public String versionCode; -    public String userId; -    public long timestamp; -    /** Response-specific data. */ -    public String extra; - -    /** -     * Parses response string into ResponseData. -     * -     * @param responseData response data string -     * @throws IllegalArgumentException upon parsing error -     * @return ResponseData object -     */ -    public static ResponseData parse(String responseData) { -        // Must parse out main response data and response-specific data. -    	int index = responseData.indexOf(':'); -    	String mainData, extraData; -    	if ( -1 == index ) { -    		mainData = responseData; -    		extraData = ""; -    	} else { -    		mainData = responseData.substring(0, index); -    		extraData = index >= responseData.length() ? "" : responseData.substring(index+1); -    	} - -        String [] fields = TextUtils.split(mainData, Pattern.quote("|")); -        if (fields.length < 6) { -            throw new IllegalArgumentException("Wrong number of fields."); -        } - -        ResponseData data = new ResponseData(); -        data.extra = extraData; -        data.responseCode = Integer.parseInt(fields[0]); -        data.nonce = Integer.parseInt(fields[1]); -        data.packageName = fields[2]; -        data.versionCode = fields[3]; -        // Application-specific user identifier. -        data.userId = fields[4]; -        data.timestamp = Long.parseLong(fields[5]); - -        return data; -    } - -    @Override -    public String toString() { -        return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode, -            userId, timestamp }); -    } -} diff --git a/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java deleted file mode 100644 index fbf8cf6d00..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java +++ /dev/null @@ -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; -    } - -} diff --git a/platform/android/java/src/com/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/android/vending/licensing/util/Base64.java deleted file mode 100644 index a0d2779af2..0000000000 --- a/platform/android/java/src/com/android/vending/licensing/util/Base64.java +++ /dev/null @@ -1,570 +0,0 @@ -// Portions copyright 2002, Google, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -//     http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.android.vending.licensing.util; - -// This code was converted from code at http://iharder.sourceforge.net/base64/ -// Lots of extraneous features were removed. -/* The original code said: - * <p> - * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit - * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> - * periodically to check for updates or to contribute improvements. - * </p> - * - * @author Robert Harder - * @author rharder@usa.net - * @version 1.3 - */ - -/** - * Base64 converter class. This code is not a full-blown MIME encoder; - * it simply converts binary data to base64 data and back. - * - * <p>Note {@link CharBase64} is a GWT-compatible implementation of this - * class. - */ -public class Base64 { -  /** Specify encoding (value is {@code true}). */ -  public final static boolean ENCODE = true; - -  /** Specify decoding (value is {@code false}). */ -  public final static boolean DECODE = false; - -  /** The equals sign (=) as a byte. */ -  private final static byte EQUALS_SIGN = (byte) '='; - -  /** The new line character (\n) as a byte. */ -  private final static byte NEW_LINE = (byte) '\n'; - -  /** -   * The 64 valid Base64 values. -   */ -  private final static byte[] ALPHABET = -      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', -          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', -          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', -          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', -          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', -          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', -          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', -          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', -          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', -          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', -          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', -          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', -          (byte) '9', (byte) '+', (byte) '/'}; - -  /** -   * The 64 valid web safe Base64 values. -   */ -  private final static byte[] WEBSAFE_ALPHABET = -      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', -          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', -          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', -          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', -          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', -          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', -          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', -          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', -          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', -          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', -          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', -          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', -          (byte) '9', (byte) '-', (byte) '_'}; - -  /** -   * Translates a Base64 value to either its 6-bit reconstruction value -   * or a negative number indicating some other meaning. -   **/ -  private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 -      -5, -5, // Whitespace: Tab and Linefeed -      -9, -9, // Decimal 11 - 12 -      -5, // Whitespace: Carriage Return -      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 -      -9, -9, -9, -9, -9, // Decimal 27 - 31 -      -5, // Whitespace: Space -      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 -      62, // Plus sign at decimal 43 -      -9, -9, -9, // Decimal 44 - 46 -      63, // Slash at decimal 47 -      52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine -      -9, -9, -9, // Decimal 58 - 60 -      -1, // Equals sign at decimal 61 -      -9, -9, -9, // Decimal 62 - 64 -      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' -      14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' -      -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 -      26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' -      39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' -      -9, -9, -9, -9, -9 // Decimal 123 - 127 -      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ -      }; - -  /** The web safe decodabet */ -  private final static byte[] WEBSAFE_DECODABET = -      {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 -          -5, -5, // Whitespace: Tab and Linefeed -          -9, -9, // Decimal 11 - 12 -          -5, // Whitespace: Carriage Return -          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 -          -9, -9, -9, -9, -9, // Decimal 27 - 31 -          -5, // Whitespace: Space -          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 -          62, // Dash '-' sign at decimal 45 -          -9, -9, // Decimal 46-47 -          52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine -          -9, -9, -9, // Decimal 58 - 60 -          -1, // Equals sign at decimal 61 -          -9, -9, -9, // Decimal 62 - 64 -          0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' -          14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' -          -9, -9, -9, -9, // Decimal 91-94 -          63, // Underscore '_' at decimal 95 -          -9, // Decimal 96 -          26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' -          39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' -          -9, -9, -9, -9, -9 // Decimal 123 - 127 -      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 -        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ -      }; - -  // Indicates white space in encoding -  private final static byte WHITE_SPACE_ENC = -5; -  // Indicates equals sign in encoding -  private final static byte EQUALS_SIGN_ENC = -1; - -  /** Defeats instantiation. */ -  private Base64() { -  } - -  /* ********  E N C O D I N G   M E T H O D S  ******** */ - -  /** -   * Encodes up to three bytes of the array <var>source</var> -   * and writes the resulting four Base64 bytes to <var>destination</var>. -   * The source and destination arrays can be manipulated -   * anywhere along their length by specifying -   * <var>srcOffset</var> and <var>destOffset</var>. -   * This method does not check to make sure your arrays -   * are large enough to accommodate <var>srcOffset</var> + 3 for -   * the <var>source</var> array or <var>destOffset</var> + 4 for -   * the <var>destination</var> array. -   * The actual number of significant bytes in your array is -   * given by <var>numSigBytes</var>. -   * -   * @param source the array to convert -   * @param srcOffset the index where conversion begins -   * @param numSigBytes the number of significant bytes in your array -   * @param destination the array to hold the conversion -   * @param destOffset the index where output will be put -   * @param alphabet is the encoding alphabet -   * @return the <var>destination</var> array -   * @since 1.3 -   */ -  private static byte[] encode3to4(byte[] source, int srcOffset, -      int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { -    //           1         2         3 -    // 01234567890123456789012345678901 Bit position -    // --------000000001111111122222222 Array position from threeBytes -    // --------|    ||    ||    ||    | Six bit groups to index alphabet -    //          >>18  >>12  >> 6  >> 0  Right shift necessary -    //                0x3f  0x3f  0x3f  Additional AND - -    // Create buffer with zero-padding if there are only one or two -    // significant bytes passed in the array. -    // We have to shift left 24 in order to flush out the 1's that appear -    // when Java treats a value as negative that is cast from a byte to an int. -    int inBuff = -        (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) -            | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) -            | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - -    switch (numSigBytes) { -      case 3: -        destination[destOffset] = alphabet[(inBuff >>> 18)]; -        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; -        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; -        destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; -        return destination; -      case 2: -        destination[destOffset] = alphabet[(inBuff >>> 18)]; -        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; -        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; -        destination[destOffset + 3] = EQUALS_SIGN; -        return destination; -      case 1: -        destination[destOffset] = alphabet[(inBuff >>> 18)]; -        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; -        destination[destOffset + 2] = EQUALS_SIGN; -        destination[destOffset + 3] = EQUALS_SIGN; -        return destination; -      default: -        return destination; -    } // end switch -  } // end encode3to4 - -  /** -   * Encodes a byte array into Base64 notation. -   * Equivalent to calling -   * {@code encodeBytes(source, 0, source.length)} -   * -   * @param source The data to convert -   * @since 1.4 -   */ -  public static String encode(byte[] source) { -    return encode(source, 0, source.length, ALPHABET, true); -  } - -  /** -   * Encodes a byte array into web safe Base64 notation. -   * -   * @param source The data to convert -   * @param doPadding is {@code true} to pad result with '=' chars -   *        if it does not fall on 3 byte boundaries -   */ -  public static String encodeWebSafe(byte[] source, boolean doPadding) { -    return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); -  } - -  /** -   * Encodes a byte array into Base64 notation. -   * -   * @param source The data to convert -   * @param off Offset in array where conversion should begin -   * @param len Length of data to convert -   * @param alphabet is the encoding alphabet -   * @param doPadding is {@code true} to pad result with '=' chars -   *        if it does not fall on 3 byte boundaries -   * @since 1.4 -   */ -  public static String encode(byte[] source, int off, int len, byte[] alphabet, -      boolean doPadding) { -    byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); -    int outLen = outBuff.length; - -    // If doPadding is false, set length to truncate '=' -    // padding characters -    while (doPadding == false && outLen > 0) { -      if (outBuff[outLen - 1] != '=') { -        break; -      } -      outLen -= 1; -    } - -    return new String(outBuff, 0, outLen); -  } - -  /** -   * Encodes a byte array into Base64 notation. -   * -   * @param source The data to convert -   * @param off Offset in array where conversion should begin -   * @param len Length of data to convert -   * @param alphabet is the encoding alphabet -   * @param maxLineLength maximum length of one line. -   * @return the BASE64-encoded byte array -   */ -  public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, -      int maxLineLength) { -    int lenDiv3 = (len + 2) / 3; // ceil(len / 3) -    int len43 = lenDiv3 * 4; -    byte[] outBuff = new byte[len43 // Main 4:3 -        + (len43 / maxLineLength)]; // New lines - -    int d = 0; -    int e = 0; -    int len2 = len - 2; -    int lineLength = 0; -    for (; d < len2; d += 3, e += 4) { - -      // The following block of code is the same as -      // encode3to4( source, d + off, 3, outBuff, e, alphabet ); -      // but inlined for faster encoding (~20% improvement) -      int inBuff = -          ((source[d + off] << 24) >>> 8) -              | ((source[d + 1 + off] << 24) >>> 16) -              | ((source[d + 2 + off] << 24) >>> 24); -      outBuff[e] = alphabet[(inBuff >>> 18)]; -      outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; -      outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; -      outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; - -      lineLength += 4; -      if (lineLength == maxLineLength) { -        outBuff[e + 4] = NEW_LINE; -        e++; -        lineLength = 0; -      } // end if: end of line -    } // end for: each piece of array - -    if (d < len) { -      encode3to4(source, d + off, len - d, outBuff, e, alphabet); - -      lineLength += 4; -      if (lineLength == maxLineLength) { -        // Add a last newline -        outBuff[e + 4] = NEW_LINE; -        e++; -      } -      e += 4; -    } - -    assert (e == outBuff.length); -    return outBuff; -  } - - -  /* ********  D E C O D I N G   M E T H O D S  ******** */ - - -  /** -   * Decodes four bytes from array <var>source</var> -   * and writes the resulting bytes (up to three of them) -   * to <var>destination</var>. -   * The source and destination arrays can be manipulated -   * anywhere along their length by specifying -   * <var>srcOffset</var> and <var>destOffset</var>. -   * This method does not check to make sure your arrays -   * are large enough to accommodate <var>srcOffset</var> + 4 for -   * the <var>source</var> array or <var>destOffset</var> + 3 for -   * the <var>destination</var> array. -   * This method returns the actual number of bytes that -   * were converted from the Base64 encoding. -   * -   * -   * @param source the array to convert -   * @param srcOffset the index where conversion begins -   * @param destination the array to hold the conversion -   * @param destOffset the index where output will be put -   * @param decodabet the decodabet for decoding Base64 content -   * @return the number of decoded bytes converted -   * @since 1.3 -   */ -  private static int decode4to3(byte[] source, int srcOffset, -      byte[] destination, int destOffset, byte[] decodabet) { -    // Example: Dk== -    if (source[srcOffset + 2] == EQUALS_SIGN) { -      int outBuff = -          ((decodabet[source[srcOffset]] << 24) >>> 6) -              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); - -      destination[destOffset] = (byte) (outBuff >>> 16); -      return 1; -    } else if (source[srcOffset + 3] == EQUALS_SIGN) { -      // Example: DkL= -      int outBuff = -          ((decodabet[source[srcOffset]] << 24) >>> 6) -              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) -              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); - -      destination[destOffset] = (byte) (outBuff >>> 16); -      destination[destOffset + 1] = (byte) (outBuff >>> 8); -      return 2; -    } else { -      // Example: DkLE -      int outBuff = -          ((decodabet[source[srcOffset]] << 24) >>> 6) -              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) -              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) -              | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); - -      destination[destOffset] = (byte) (outBuff >> 16); -      destination[destOffset + 1] = (byte) (outBuff >> 8); -      destination[destOffset + 2] = (byte) (outBuff); -      return 3; -    } -  } // end decodeToBytes - - -  /** -   * Decodes data from Base64 notation. -   * -   * @param s the string to decode (decoded in default encoding) -   * @return the decoded data -   * @since 1.4 -   */ -  public static byte[] decode(String s) throws Base64DecoderException { -    byte[] bytes = s.getBytes(); -    return decode(bytes, 0, bytes.length); -  } - -  /** -   * Decodes data from web safe Base64 notation. -   * Web safe encoding uses '-' instead of '+', '_' instead of '/' -   * -   * @param s the string to decode (decoded in default encoding) -   * @return the decoded data -   */ -  public static byte[] decodeWebSafe(String s) throws Base64DecoderException { -    byte[] bytes = s.getBytes(); -    return decodeWebSafe(bytes, 0, bytes.length); -  } - -  /** -   * Decodes Base64 content in byte array format and returns -   * the decoded byte array. -   * -   * @param source The Base64 encoded data -   * @return decoded data -   * @since 1.3 -   * @throws Base64DecoderException -   */ -  public static byte[] decode(byte[] source) throws Base64DecoderException { -    return decode(source, 0, source.length); -  } - -  /** -   * Decodes web safe Base64 content in byte array format and returns -   * the decoded data. -   * Web safe encoding uses '-' instead of '+', '_' instead of '/' -   * -   * @param source the string to decode (decoded in default encoding) -   * @return the decoded data -   */ -  public static byte[] decodeWebSafe(byte[] source) -      throws Base64DecoderException { -    return decodeWebSafe(source, 0, source.length); -  } - -  /** -   * Decodes Base64 content in byte array format and returns -   * the decoded byte array. -   * -   * @param source The Base64 encoded data -   * @param off    The offset of where to begin decoding -   * @param len    The length of characters to decode -   * @return decoded data -   * @since 1.3 -   * @throws Base64DecoderException -   */ -  public static byte[] decode(byte[] source, int off, int len) -      throws Base64DecoderException { -    return decode(source, off, len, DECODABET); -  } - -  /** -   * Decodes web safe Base64 content in byte array format and returns -   * the decoded byte array. -   * Web safe encoding uses '-' instead of '+', '_' instead of '/' -   * -   * @param source The Base64 encoded data -   * @param off    The offset of where to begin decoding -   * @param len    The length of characters to decode -   * @return decoded data -   */ -  public static byte[] decodeWebSafe(byte[] source, int off, int len) -      throws Base64DecoderException { -    return decode(source, off, len, WEBSAFE_DECODABET); -  } - -  /** -   * Decodes Base64 content using the supplied decodabet and returns -   * the decoded byte array. -   * -   * @param source    The Base64 encoded data -   * @param off       The offset of where to begin decoding -   * @param len       The length of characters to decode -   * @param decodabet the decodabet for decoding Base64 content -   * @return decoded data -   */ -  public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) -      throws Base64DecoderException { -    int len34 = len * 3 / 4; -    byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output -    int outBuffPosn = 0; - -    byte[] b4 = new byte[4]; -    int b4Posn = 0; -    int i = 0; -    byte sbiCrop = 0; -    byte sbiDecode = 0; -    for (i = 0; i < len; i++) { -      sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits -      sbiDecode = decodabet[sbiCrop]; - -      if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better -        if (sbiDecode >= EQUALS_SIGN_ENC) { -          // An equals sign (for padding) must not occur at position 0 or 1 -          // and must be the last byte[s] in the encoded value -          if (sbiCrop == EQUALS_SIGN) { -            int bytesLeft = len - i; -            byte lastByte = (byte) (source[len - 1 + off] & 0x7f); -            if (b4Posn == 0 || b4Posn == 1) { -              throw new Base64DecoderException( -                  "invalid padding byte '=' at byte offset " + i); -            } else if ((b4Posn == 3 && bytesLeft > 2) -                || (b4Posn == 4 && bytesLeft > 1)) { -              throw new Base64DecoderException( -                  "padding byte '=' falsely signals end of encoded value " -                      + "at offset " + i); -            } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { -              throw new Base64DecoderException( -                  "encoded value has invalid trailing byte"); -            } -            break; -          } - -          b4[b4Posn++] = sbiCrop; -          if (b4Posn == 4) { -            outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); -            b4Posn = 0; -          } -        } -      } else { -        throw new Base64DecoderException("Bad Base64 input character at " + i -            + ": " + source[i + off] + "(decimal)"); -      } -    } - -    // Because web safe encoding allows non padding base64 encodes, we -    // need to pad the rest of the b4 buffer with equal signs when -    // b4Posn != 0.  There can be at most 2 equal signs at the end of -    // four characters, so the b4 buffer must have two or three -    // characters.  This also catches the case where the input is -    // padded with EQUALS_SIGN -    if (b4Posn != 0) { -      if (b4Posn == 1) { -        throw new Base64DecoderException("single trailing character at offset " -            + (len - 1)); -      } -      b4[b4Posn++] = EQUALS_SIGN; -      outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); -    } - -    byte[] out = new byte[outBuffPosn]; -    System.arraycopy(outBuff, 0, out, 0, outBuffPosn); -    return out; -  } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java index 2af33b96b9..ff1eee528f 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java +++ b/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;  }
\ No newline at end of file diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java index 9cb294d721..9a78a6d3df 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java +++ b/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]; +		} +	};  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java index 2201751254..146426ef83 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java index 054eaa9895..f75debe32d 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java index fb56f917be..cd8726533f 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +++ b/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; +		} +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java index b8511a62a0..bae93f633a 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java +++ b/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);  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java index 4789afe19c..a84fb32728 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java +++ b/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);  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java index d5bc3a843e..dcdef1bfcf 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java +++ b/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);  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java index 12edd97ab2..c5577d4c2a 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +++ b/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(); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java deleted file mode 100644 index 4667acce67..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java +++ /dev/null @@ -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); -    } -}
\ No newline at end of file diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java index b77af7e085..6346d7703a 100755 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java +++ b/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"); +			} +		} +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java deleted file mode 100644 index e2673a9dd7..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java +++ /dev/null @@ -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(); -    } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java index 45111b16a3..0e72b7ae77 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java index a9f674803c..099e3f05b3 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +++ b/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) { +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java index 056d1eca0b..2fa146408b 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java index e83faa2756..25a561ccd4 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java index 250299c400..5d8dce0bac 100755 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java +++ b/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(); +			} +		} +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java index 3f440e9893..02bd1f27f6 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java +++ b/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); +	}  } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java deleted file mode 100644 index 56b2331e31..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java +++ /dev/null @@ -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; -    } - -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java new file mode 100644 index 0000000000..feba3034c3 --- /dev/null +++ b/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); +		} +	} +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000000..2c60e7e4b8 --- /dev/null +++ b/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; +	} +} diff --git a/platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java b/platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java index e5c5e2d7ca..2384b8b82f 100644 --- a/platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java +++ b/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);  } diff --git a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl index c816558afc..c816558afc 100644 --- a/platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java new file mode 100644 index 0000000000..89edeae1b4 --- /dev/null +++ b/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; +} diff --git a/platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl index 664510ce0c..664510ce0c 100644 --- a/platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl +++ b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java new file mode 100644 index 0000000000..8b7cc83541 --- /dev/null +++ b/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; +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..38aab9f4f5 --- /dev/null +++ b/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 ""; +		} +	} +} diff --git a/platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java index b250a7147b..dc2c2d70bf 100644 --- a/platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java +++ b/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);  } diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java new file mode 100644 index 0000000000..77f7dc7295 --- /dev/null +++ b/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); +	} +} diff --git a/platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java b/platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java index d87af3153f..a43e454228 100644 --- a/platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java +++ b/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; +	}  } diff --git a/platform/android/java/src/com/android/vending/licensing/Obfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java index 88891728e6..8731d03aa6 100644 --- a/platform/android/java/src/com/android/vending/licensing/Obfuscator.java +++ b/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;  } diff --git a/platform/android/java/src/com/android/vending/licensing/Policy.java b/platform/android/java/src/com/google/android/vending/licensing/Policy.java index fa267fc71a..65202aceb9 100644 --- a/platform/android/java/src/com/android/vending/licensing/Policy.java +++ b/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();  } diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java new file mode 100644 index 0000000000..099bb1c48b --- /dev/null +++ b/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; +		} +	} +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java new file mode 100644 index 0000000000..1c802f8e45 --- /dev/null +++ b/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 }); +	} +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000000..b9a50c1104 --- /dev/null +++ b/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; +	} +} diff --git a/platform/android/java/src/com/android/vending/licensing/StrictPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java index d8d83b4e4b..9849730c38 100644 --- a/platform/android/java/src/com/android/vending/licensing/StrictPolicy.java +++ b/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; +	}  } diff --git a/platform/android/java/src/com/android/vending/licensing/ValidationException.java b/platform/android/java/src/com/google/android/vending/licensing/ValidationException.java index ee4df47c68..79b70e6804 100644 --- a/platform/android/java/src/com/android/vending/licensing/ValidationException.java +++ b/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;  } diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java new file mode 100644 index 0000000000..bd711aadf5 --- /dev/null +++ b/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 rharder@usa.net + * @version 1.3 + */ + +import com.godot.game.BuildConfig; + +/** + * Base64 converter class. This code is not a full-blown MIME encoder; + * it simply converts binary data to base64 data and back. + * + * <p>Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { +	/** Specify encoding (value is {@code true}). */ +	public final static boolean ENCODE = true; + +	/** Specify decoding (value is {@code false}). */ +	public final static boolean DECODE = false; + +	/** The equals sign (=) as a byte. */ +	private final static byte EQUALS_SIGN = (byte)'='; + +	/** The new line character (\n) as a byte. */ +	private final static byte NEW_LINE = (byte)'\n'; + +	/** +   * The 64 valid Base64 values. +   */ +	private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', +		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', +		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', +		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', +		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', +		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', +		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', +		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', +		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', +		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', +		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', +		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', +		(byte)'9', (byte)'+', (byte)'/' }; + +	/** +   * The 64 valid web safe Base64 values. +   */ +	private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', +		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', +		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', +		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', +		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', +		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', +		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', +		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', +		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', +		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', +		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', +		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', +		(byte)'9', (byte)'-', (byte)'_' }; + +	/** +   * Translates a Base64 value to either its 6-bit reconstruction value +   * or a negative number indicating some other meaning. +   **/ +	private final static byte[] DECODABET = { +		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 +		-5, -5, // Whitespace: Tab and Linefeed +		-9, -9, // Decimal 11 - 12 +		-5, // Whitespace: Carriage Return +		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 +		-9, -9, -9, -9, -9, // Decimal 27 - 31 +		-5, // Whitespace: Space +		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 +		62, // Plus sign at decimal 43 +		-9, -9, -9, // Decimal 44 - 46 +		63, // Slash at decimal 47 +		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine +		-9, -9, -9, // Decimal 58 - 60 +		-1, // Equals sign at decimal 61 +		-9, -9, -9, // Decimal 62 - 64 +		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' +		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' +		-9, -9, -9, -9, -9, -9, // Decimal 91 - 96 +		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' +		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' +		-9, -9, -9, -9, -9 // Decimal 123 - 127 +		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ +	}; + +	/** The web safe decodabet */ +	private final static byte[] WEBSAFE_DECODABET = { +		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 +		-5, -5, // Whitespace: Tab and Linefeed +		-9, -9, // Decimal 11 - 12 +		-5, // Whitespace: Carriage Return +		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 +		-9, -9, -9, -9, -9, // Decimal 27 - 31 +		-5, // Whitespace: Space +		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 +		62, // Dash '-' sign at decimal 45 +		-9, -9, // Decimal 46-47 +		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine +		-9, -9, -9, // Decimal 58 - 60 +		-1, // Equals sign at decimal 61 +		-9, -9, -9, // Decimal 62 - 64 +		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' +		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' +		-9, -9, -9, -9, // Decimal 91-94 +		63, // Underscore '_' at decimal 95 +		-9, // Decimal 96 +		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' +		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' +		-9, -9, -9, -9, -9 // Decimal 123 - 127 +		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ +	}; + +	// Indicates white space in encoding +	private final static byte WHITE_SPACE_ENC = -5; +	// Indicates equals sign in encoding +	private final static byte EQUALS_SIGN_ENC = -1; + +	/** Defeats instantiation. */ +	private Base64() { +	} + +	/* ********  E N C O D I N G   M E T H O D S  ******** */ + +	/** +   * Encodes up to three bytes of the array <var>source</var> +   * and writes the resulting four Base64 bytes to <var>destination</var>. +   * The source and destination arrays can be manipulated +   * anywhere along their length by specifying +   * <var>srcOffset</var> and <var>destOffset</var>. +   * This method does not check to make sure your arrays +   * are large enough to accommodate <var>srcOffset</var> + 3 for +   * the <var>source</var> array or <var>destOffset</var> + 4 for +   * the <var>destination</var> array. +   * The actual number of significant bytes in your array is +   * given by <var>numSigBytes</var>. +   * +   * @param source the array to convert +   * @param srcOffset the index where conversion begins +   * @param numSigBytes the number of significant bytes in your array +   * @param destination the array to hold the conversion +   * @param destOffset the index where output will be put +   * @param alphabet is the encoding alphabet +   * @return the <var>destination</var> array +   * @since 1.3 +   */ +	private static byte[] encode3to4(byte[] source, int srcOffset, +			int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { +		//           1         2         3 +		// 01234567890123456789012345678901 Bit position +		// --------000000001111111122222222 Array position from threeBytes +		// --------|    ||    ||    ||    | Six bit groups to index alphabet +		//          >>18  >>12  >> 6  >> 0  Right shift necessary +		//                0x3f  0x3f  0x3f  Additional AND + +		// Create buffer with zero-padding if there are only one or two +		// significant bytes passed in the array. +		// We have to shift left 24 in order to flush out the 1's that appear +		// when Java treats a value as negative that is cast from a byte to an int. +		int inBuff = +				(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + +		switch (numSigBytes) { +			case 3: +				destination[destOffset] = alphabet[(inBuff >>> 18)]; +				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +				destination[destOffset + 3] = alphabet[(inBuff)&0x3f]; +				return destination; +			case 2: +				destination[destOffset] = alphabet[(inBuff >>> 18)]; +				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +				destination[destOffset + 3] = EQUALS_SIGN; +				return destination; +			case 1: +				destination[destOffset] = alphabet[(inBuff >>> 18)]; +				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +				destination[destOffset + 2] = EQUALS_SIGN; +				destination[destOffset + 3] = EQUALS_SIGN; +				return destination; +			default: +				return destination; +		} // end switch +	} // end encode3to4 + +	/** +   * Encodes a byte array into Base64 notation. +   * Equivalent to calling +   * {@code encodeBytes(source, 0, source.length)} +   * +   * @param source The data to convert +   * @since 1.4 +   */ +	public static String encode(byte[] source) { +		return encode(source, 0, source.length, ALPHABET, true); +	} + +	/** +   * Encodes a byte array into web safe Base64 notation. +   * +   * @param source The data to convert +   * @param doPadding is {@code true} to pad result with '=' chars +   *        if it does not fall on 3 byte boundaries +   */ +	public static String encodeWebSafe(byte[] source, boolean doPadding) { +		return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); +	} + +	/** +   * Encodes a byte array into Base64 notation. +   * +   * @param source The data to convert +   * @param off Offset in array where conversion should begin +   * @param len Length of data to convert +   * @param alphabet is the encoding alphabet +   * @param doPadding is {@code true} to pad result with '=' chars +   *        if it does not fall on 3 byte boundaries +   * @since 1.4 +   */ +	public static String encode(byte[] source, int off, int len, byte[] alphabet, +			boolean doPadding) { +		byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); +		int outLen = outBuff.length; + +		// If doPadding is false, set length to truncate '=' +		// padding characters +		while (doPadding == false && outLen > 0) { +			if (outBuff[outLen - 1] != '=') { +				break; +			} +			outLen -= 1; +		} + +		return new String(outBuff, 0, outLen); +	} + +	/** +   * Encodes a byte array into Base64 notation. +   * +   * @param source The data to convert +   * @param off Offset in array where conversion should begin +   * @param len Length of data to convert +   * @param alphabet is the encoding alphabet +   * @param maxLineLength maximum length of one line. +   * @return the BASE64-encoded byte array +   */ +	public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, +			int maxLineLength) { +		int lenDiv3 = (len + 2) / 3; // ceil(len / 3) +		int len43 = lenDiv3 * 4; +		byte[] outBuff = new byte[len43 // Main 4:3 +								  + (len43 / maxLineLength)]; // New lines + +		int d = 0; +		int e = 0; +		int len2 = len - 2; +		int lineLength = 0; +		for (; d < len2; d += 3, e += 4) { + +			// The following block of code is the same as +			// encode3to4( source, d + off, 3, outBuff, e, alphabet ); +			// but inlined for faster encoding (~20% improvement) +			int inBuff = +					((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24); +			outBuff[e] = alphabet[(inBuff >>> 18)]; +			outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +			outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +			outBuff[e + 3] = alphabet[(inBuff)&0x3f]; + +			lineLength += 4; +			if (lineLength == maxLineLength) { +				outBuff[e + 4] = NEW_LINE; +				e++; +				lineLength = 0; +			} // end if: end of line +		} // end for: each piece of array + +		if (d < len) { +			encode3to4(source, d + off, len - d, outBuff, e, alphabet); + +			lineLength += 4; +			if (lineLength == maxLineLength) { +				// Add a last newline +				outBuff[e + 4] = NEW_LINE; +				e++; +			} +			e += 4; +		} + +		if (BuildConfig.DEBUG && e != outBuff.length) +			throw new RuntimeException(); +		return outBuff; +	} + +	/* ********  D E C O D I N G   M E T H O D S  ******** */ + +	/** +   * Decodes four bytes from array <var>source</var> +   * and writes the resulting bytes (up to three of them) +   * to <var>destination</var>. +   * The source and destination arrays can be manipulated +   * anywhere along their length by specifying +   * <var>srcOffset</var> and <var>destOffset</var>. +   * This method does not check to make sure your arrays +   * are large enough to accommodate <var>srcOffset</var> + 4 for +   * the <var>source</var> array or <var>destOffset</var> + 3 for +   * the <var>destination</var> array. +   * This method returns the actual number of bytes that +   * were converted from the Base64 encoding. +   * +   * +   * @param source the array to convert +   * @param srcOffset the index where conversion begins +   * @param destination the array to hold the conversion +   * @param destOffset the index where output will be put +   * @param decodabet the decodabet for decoding Base64 content +   * @return the number of decoded bytes converted +   * @since 1.3 +   */ +	private static int decode4to3(byte[] source, int srcOffset, +			byte[] destination, int destOffset, byte[] decodabet) { +		// Example: Dk== +		if (source[srcOffset + 2] == EQUALS_SIGN) { +			int outBuff = +					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + +			destination[destOffset] = (byte)(outBuff >>> 16); +			return 1; +		} else if (source[srcOffset + 3] == EQUALS_SIGN) { +			// Example: DkL= +			int outBuff = +					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + +			destination[destOffset] = (byte)(outBuff >>> 16); +			destination[destOffset + 1] = (byte)(outBuff >>> 8); +			return 2; +		} else { +			// Example: DkLE +			int outBuff = +					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + +			destination[destOffset] = (byte)(outBuff >> 16); +			destination[destOffset + 1] = (byte)(outBuff >> 8); +			destination[destOffset + 2] = (byte)(outBuff); +			return 3; +		} +	} // end decodeToBytes + +	/** +   * Decodes data from Base64 notation. +   * +   * @param s the string to decode (decoded in default encoding) +   * @return the decoded data +   * @since 1.4 +   */ +	public static byte[] decode(String s) throws Base64DecoderException { +		byte[] bytes = s.getBytes(); +		return decode(bytes, 0, bytes.length); +	} + +	/** +   * Decodes data from web safe Base64 notation. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param s the string to decode (decoded in default encoding) +   * @return the decoded data +   */ +	public static byte[] decodeWebSafe(String s) throws Base64DecoderException { +		byte[] bytes = s.getBytes(); +		return decodeWebSafe(bytes, 0, bytes.length); +	} + +	/** +   * Decodes Base64 content in byte array format and returns +   * the decoded byte array. +   * +   * @param source The Base64 encoded data +   * @return decoded data +   * @since 1.3 +   * @throws Base64DecoderException +   */ +	public static byte[] decode(byte[] source) throws Base64DecoderException { +		return decode(source, 0, source.length); +	} + +	/** +   * Decodes web safe Base64 content in byte array format and returns +   * the decoded data. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param source the string to decode (decoded in default encoding) +   * @return the decoded data +   */ +	public static byte[] decodeWebSafe(byte[] source) +			throws Base64DecoderException { +		return decodeWebSafe(source, 0, source.length); +	} + +	/** +   * Decodes Base64 content in byte array format and returns +   * the decoded byte array. +   * +   * @param source The Base64 encoded data +   * @param off    The offset of where to begin decoding +   * @param len    The length of characters to decode +   * @return decoded data +   * @since 1.3 +   * @throws Base64DecoderException +   */ +	public static byte[] decode(byte[] source, int off, int len) +			throws Base64DecoderException { +		return decode(source, off, len, DECODABET); +	} + +	/** +   * Decodes web safe Base64 content in byte array format and returns +   * the decoded byte array. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param source The Base64 encoded data +   * @param off    The offset of where to begin decoding +   * @param len    The length of characters to decode +   * @return decoded data +   */ +	public static byte[] decodeWebSafe(byte[] source, int off, int len) +			throws Base64DecoderException { +		return decode(source, off, len, WEBSAFE_DECODABET); +	} + +	/** +   * Decodes Base64 content using the supplied decodabet and returns +   * the decoded byte array. +   * +   * @param source    The Base64 encoded data +   * @param off       The offset of where to begin decoding +   * @param len       The length of characters to decode +   * @param decodabet the decodabet for decoding Base64 content +   * @return decoded data +   */ +	public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) +			throws Base64DecoderException { +		int len34 = len * 3 / 4; +		byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output +		int outBuffPosn = 0; + +		byte[] b4 = new byte[4]; +		int b4Posn = 0; +		int i = 0; +		byte sbiCrop = 0; +		byte sbiDecode = 0; +		for (i = 0; i < len; i++) { +			sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits +			sbiDecode = decodabet[sbiCrop]; + +			if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better +				if (sbiDecode >= EQUALS_SIGN_ENC) { +					// An equals sign (for padding) must not occur at position 0 or 1 +					// and must be the last byte[s] in the encoded value +					if (sbiCrop == EQUALS_SIGN) { +						int bytesLeft = len - i; +						byte lastByte = (byte)(source[len - 1 + off] & 0x7f); +						if (b4Posn == 0 || b4Posn == 1) { +							throw new Base64DecoderException( +									"invalid padding byte '=' at byte offset " + i); +						} else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) { +							throw new Base64DecoderException( +									"padding byte '=' falsely signals end of encoded value " +									+ "at offset " + i); +						} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { +							throw new Base64DecoderException( +									"encoded value has invalid trailing byte"); +						} +						break; +					} + +					b4[b4Posn++] = sbiCrop; +					if (b4Posn == 4) { +						outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); +						b4Posn = 0; +					} +				} +			} else { +				throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)"); +			} +		} + +		// Because web safe encoding allows non padding base64 encodes, we +		// need to pad the rest of the b4 buffer with equal signs when +		// b4Posn != 0.  There can be at most 2 equal signs at the end of +		// four characters, so the b4 buffer must have two or three +		// characters.  This also catches the case where the input is +		// padded with EQUALS_SIGN +		if (b4Posn != 0) { +			if (b4Posn == 1) { +				throw new Base64DecoderException("single trailing character at offset " + (len - 1)); +			} +			b4[b4Posn++] = EQUALS_SIGN; +			outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); +		} + +		byte[] out = new byte[outBuffPosn]; +		System.arraycopy(outBuff, 0, out, 0, outBuffPosn); +		return out; +	} +} diff --git a/platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java index 1aef1b54b8..50724a9b05 100644 --- a/platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java +++ b/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;  } diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java b/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000000..4f908b472c --- /dev/null +++ b/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."); +		} +	} +} diff --git a/platform/android/java/src/org/godotengine/godot/Godot.java b/platform/android/java/src/org/godotengine/godot/Godot.java index 88194f00d1..e3878754a0 100644 --- a/platform/android/java/src/org/godotengine/godot/Godot.java +++ b/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));  	} diff --git a/platform/android/java/src/org/godotengine/godot/GodotIO.java b/platform/android/java/src/org/godotengine/godot/GodotIO.java index a95c508d21..75d67831d4 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotIO.java +++ b/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) { diff --git a/platform/android/java/src/org/godotengine/godot/GodotView.java b/platform/android/java/src/org/godotengine/godot/GodotView.java index 4cb4db33de..da5a8b11e2 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotView.java +++ b/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) { diff --git a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java index 53fcf5ef70..b2ac2c0d67 100644 --- a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java +++ b/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; +		}  	}  	// =========================================================== diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java index 0a876d2b7f..4042c42e9d 100644 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java +++ b/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);  		}  	}  } diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java b/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java deleted file mode 100644 index a1418c5899..0000000000 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java +++ /dev/null @@ -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); -	} -} diff --git a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java b/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java index 5d94e77cd7..fe67f42f19 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java +++ b/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); diff --git a/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java b/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java deleted file mode 100644 index 8b48193ae2..0000000000 --- a/platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java +++ /dev/null @@ -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); -} diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java index 40cdeea72e..715f6e6f93 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java +++ b/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) { diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java index d4c7380424..b3b27ec1b9 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/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; diff --git a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java b/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java index eccc6f671b..fd6fff3fa9 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java +++ b/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(); diff --git a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java index 0626e50bb1..2d1c467235 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java +++ b/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(); diff --git a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java b/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java index cfe9c4fef0..e1958390a5 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java +++ b/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) {  |