summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--platform/android/AndroidManifest.xml.template22
-rw-r--r--platform/android/SCsub2
-rw-r--r--platform/android/build.gradle.template7
-rw-r--r--platform/android/export/export.cpp2
-rw-r--r--platform/android/java/README.md47
-rw-r--r--platform/android/java/gradle/wrapper/gradle-wrapper.jarbin53636 -> 54329 bytes
-rw-r--r--platform/android/java/gradle/wrapper/gradle-wrapper.properties3
-rwxr-xr-xplatform/android/java/gradlew72
-rw-r--r--platform/android/java/gradlew.bat174
-rw-r--r--platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.pngbin462 -> 1843 bytes
-rw-r--r--platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.pngbin127 -> 718 bytes
-rw-r--r--platform/android/java/res/drawable-nodpi/icon.png (renamed from platform/android/java/res/drawable/icon.png)bin7569 -> 7569 bytes
-rw-r--r--platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.pngbin0 -> 462 bytes
-rw-r--r--platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.pngbin0 -> 2830 bytes
-rw-r--r--platform/android/java/res/layout/downloading_expansion.xml47
-rw-r--r--platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml26
-rw-r--r--platform/android/java/res/values-ko/strings.xml2
-rw-r--r--platform/android/java/res/values-v11/styles.xml6
-rw-r--r--platform/android/java/res/values-v9/styles.xml5
-rw-r--r--platform/android/java/res/values/strings.xml2
-rw-r--r--platform/android/java/src/com/android/vending/licensing/AESObfuscator.java110
-rw-r--r--platform/android/java/src/com/android/vending/licensing/APKExpansionPolicy.java397
-rw-r--r--platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.java115
-rw-r--r--platform/android/java/src/com/android/vending/licensing/ILicensingService.java115
-rw-r--r--platform/android/java/src/com/android/vending/licensing/LicenseChecker.java351
-rw-r--r--platform/android/java/src/com/android/vending/licensing/LicenseValidator.java224
-rw-r--r--platform/android/java/src/com/android/vending/licensing/PreferenceObfuscator.java77
-rw-r--r--platform/android/java/src/com/android/vending/licensing/ResponseData.java79
-rw-r--r--platform/android/java/src/com/android/vending/licensing/ServerManagedPolicy.java276
-rw-r--r--platform/android/java/src/com/android/vending/licensing/util/Base64.java570
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java177
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java80
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java317
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java270
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java486
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java62
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java32
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java6
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java169
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/AndroidHttpClient.java536
-rwxr-xr-xplatform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java131
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomNotificationFactory.java30
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java112
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java352
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java1530
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java1941
-rwxr-xr-xplatform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java887
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java295
-rw-r--r--platform/android/java/src/com/google/android/vending/expansion/downloader/impl/V14CustomNotification.java101
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java110
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java413
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java (renamed from platform/android/java/src/com/android/vending/licensing/DeviceLimiter.java)4
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl (renamed from platform/android/java/src/com/android/vending/licensing/ILicenseResultListener.aidl)0
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java100
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl (renamed from platform/android/java/src/com/android/vending/licensing/ILicensingService.aidl)0
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java100
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java387
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java (renamed from platform/android/java/src/com/android/vending/licensing/LicenseCheckerCallback.java)30
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java232
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java (renamed from platform/android/java/src/com/android/vending/licensing/NullDeviceLimiter.java)6
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java (renamed from platform/android/java/src/com/android/vending/licensing/Obfuscator.java)16
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/Policy.java (renamed from platform/android/java/src/com/android/vending/licensing/Policy.java)30
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java78
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ResponseData.java80
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java299
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java (renamed from platform/android/java/src/com/android/vending/licensing/StrictPolicy.java)66
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/ValidationException.java (renamed from platform/android/java/src/com/android/vending/licensing/ValidationException.java)14
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/util/Base64.java556
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java (renamed from platform/android/java/src/com/android/vending/licensing/util/Base64DecoderException.java)14
-rw-r--r--platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java60
-rw-r--r--platform/android/java/src/org/godotengine/godot/Godot.java148
-rw-r--r--platform/android/java/src/org/godotengine/godot/GodotIO.java38
-rw-r--r--platform/android/java/src/org/godotengine/godot/GodotView.java59
-rw-r--r--platform/android/java/src/org/godotengine/godot/input/GodotEditText.java72
-rw-r--r--platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java7
-rw-r--r--platform/android/java/src/org/godotengine/godot/input/InputManagerV9.java209
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java88
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/GenericConsumeTask.java79
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java4
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java4
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java82
-rw-r--r--platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java129
-rw-r--r--platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java4
83 files changed, 6472 insertions, 7291 deletions
diff --git a/platform/android/AndroidManifest.xml.template b/platform/android/AndroidManifest.xml.template
index 81f4c15849..d8fb9326ea 100644
--- a/platform/android/AndroidManifest.xml.template
+++ b/platform/android/AndroidManifest.xml.template
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.godot.game"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.godot.game"
android:versionCode="1"
android:versionName="1.0"
android:installLocation="auto"
@@ -10,32 +11,29 @@
android:largeScreens="true"
android:xlargeScreens="true"/>
- <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" $$ADD_APPATTRIBUTE_CHUNKS$$ >
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+$$ADD_PERMISSION_CHUNKS$$
+
+ <application android:label="@string/godot_project_name_string" android:icon="@drawable/icon" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" $$ADD_APPATTRIBUTE_CHUNKS$$ >
<activity android:name="org.godotengine.godot.Godot"
android:label="@string/godot_project_name_string"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize"
- android:resizeableActivity="false">
+ android:resizeableActivity="false"
+ tools:ignore="UnusedAttribute">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <service android:name="org.godotengine.godot.GodotDownloaderService" />
-
-
-
+ <service android:name="org.godotengine.godot.GodotDownloaderService" />
$$ADD_APPLICATION_CHUNKS$$
</application>
- <uses-feature android:glEsVersion="0x00020000" android:required="true" />
-
-$$ADD_PERMISSION_CHUNKS$$
-
-<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="27"/>
</manifest>
diff --git a/platform/android/SCsub b/platform/android/SCsub
index 6d5af99bc5..fb0ec75b16 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -108,7 +108,7 @@ for x in env.android_asset_dirs:
gradle_default_config_text = ""
minSdk = 18
-targetSdk = 27
+targetSdk = 28
for x in env.android_default_config:
if x.startswith("minSdkVersion") and int(x.split(" ")[-1]) < minSdk:
diff --git a/platform/android/build.gradle.template b/platform/android/build.gradle.template
index 18ffc74fc3..2fea250061 100644
--- a/platform/android/build.gradle.template
+++ b/platform/android/build.gradle.template
@@ -5,7 +5,7 @@ buildscript {
$$GRADLE_REPOSITORY_URLS$$
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.0'
+ classpath 'com.android.tools.build:gradle:3.2.1'
$$GRADLE_CLASSPATH$$
}
}
@@ -22,6 +22,7 @@ allprojects {
}
dependencies {
+ implementation "com.android.support:support-core-utils:28.0.0"
$$GRADLE_DEPENDENCIES$$
}
@@ -29,10 +30,10 @@ android {
lintOptions {
abortOnError false
- disable 'MissingTranslation'
+ disable 'MissingTranslation','UnusedResources'
}
- compileSdkVersion 27
+ compileSdkVersion 28
buildToolsVersion "28.0.3"
useLibrary 'org.apache.http.legacy'
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index 7eda32b754..7f8d50b8ab 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -1564,7 +1564,7 @@ public:
_fix_resources(p_preset, data);
}
- if (file == "res/drawable/icon.png") {
+ if (file == "res/drawable-nodpi-v4/icon.png") {
bool found = false;
for (unsigned int i = 0; i < sizeof(launcher_icons) / sizeof(launcher_icons[0]); ++i) {
String icon_path = String(p_preset->get(launcher_icons[i].option_id)).strip_edges();
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
index 13372aef5e..f6b961fd5a 100644
--- a/platform/android/java/gradle/wrapper/gradle-wrapper.jar
+++ b/platform/android/java/gradle/wrapper/gradle-wrapper.jar
Binary files differ
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
index 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
Binary files differ
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
index 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
Binary files differ
diff --git a/platform/android/java/res/drawable/icon.png b/platform/android/java/res/drawable-nodpi/icon.png
index 6ad9b43117..6ad9b43117 100644
--- a/platform/android/java/res/drawable/icon.png
+++ b/platform/android/java/res/drawable-nodpi/icon.png
Binary files differ
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
new file mode 100644
index 0000000000..372b763ec5
--- /dev/null
+++ b/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png
Binary files differ
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
new file mode 100644
index 0000000000..b458ff3057
--- /dev/null
+++ b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png
Binary files differ
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) {