Fix lint error/warning while building android template
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<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:versionCode="1"
|
||||||
android:versionName="1.0"
|
android:versionName="1.0"
|
||||||
android:installLocation="auto"
|
android:installLocation="auto"
|
||||||
@ -10,32 +11,29 @@
|
|||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
android:xlargeScreens="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"
|
<activity android:name="org.godotengine.godot.Godot"
|
||||||
android:label="@string/godot_project_name_string"
|
android:label="@string/godot_project_name_string"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize"
|
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize"
|
||||||
android:resizeableActivity="false">
|
android:resizeableActivity="false"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name="org.godotengine.godot.GodotDownloaderService" />
|
<service android:name="org.godotengine.godot.GodotDownloaderService" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$$ADD_APPLICATION_CHUNKS$$
|
$$ADD_APPLICATION_CHUNKS$$
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
|
||||||
|
|
||||||
$$ADD_PERMISSION_CHUNKS$$
|
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="27"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -108,7 +108,7 @@ for x in env.android_asset_dirs:
|
|||||||
gradle_default_config_text = ""
|
gradle_default_config_text = ""
|
||||||
|
|
||||||
minSdk = 18
|
minSdk = 18
|
||||||
targetSdk = 27
|
targetSdk = 28
|
||||||
|
|
||||||
for x in env.android_default_config:
|
for x in env.android_default_config:
|
||||||
if x.startswith("minSdkVersion") and int(x.split(" ")[-1]) < minSdk:
|
if x.startswith("minSdkVersion") and int(x.split(" ")[-1]) < minSdk:
|
||||||
|
@ -5,7 +5,7 @@ buildscript {
|
|||||||
$$GRADLE_REPOSITORY_URLS$$
|
$$GRADLE_REPOSITORY_URLS$$
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.2.0'
|
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||||
$$GRADLE_CLASSPATH$$
|
$$GRADLE_CLASSPATH$$
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,6 +22,7 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "com.android.support:support-core-utils:28.0.0"
|
||||||
$$GRADLE_DEPENDENCIES$$
|
$$GRADLE_DEPENDENCIES$$
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,10 +30,10 @@ android {
|
|||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
abortOnError false
|
abortOnError false
|
||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation','UnusedResources'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdkVersion 27
|
compileSdkVersion 28
|
||||||
buildToolsVersion "28.0.3"
|
buildToolsVersion "28.0.3"
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
|
@ -1564,7 +1564,7 @@ public:
|
|||||||
_fix_resources(p_preset, data);
|
_fix_resources(p_preset, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file == "res/drawable/icon.png") {
|
if (file == "res/drawable-nodpi-v4/icon.png") {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (unsigned int i = 0; i < sizeof(launcher_icons) / sizeof(launcher_icons[0]); ++i) {
|
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();
|
String icon_path = String(p_preset->get(launcher_icons[i].option_id)).strip_edges();
|
||||||
|
47
platform/android/java/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Third party libraries
|
||||||
|
|
||||||
|
|
||||||
|
## Google's vending library
|
||||||
|
|
||||||
|
- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/src/main/java/com/google/android/vending
|
||||||
|
- Version: git (eb57657, 2018) with modifications
|
||||||
|
- License: Apache 2.0
|
||||||
|
|
||||||
|
Overwrite all files under `com/google/android/vending`
|
||||||
|
|
||||||
|
### Modify some files to avoid compile error and lint warning
|
||||||
|
|
||||||
|
#### com/google/android/vending/licensing/util/Base64.java
|
||||||
|
```
|
||||||
|
@@ -338,7 +338,8 @@ public class Base64 {
|
||||||
|
e += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
- assert (e == outBuff.length);
|
||||||
|
+ if (BuildConfig.DEBUG && e != outBuff.length)
|
||||||
|
+ throw new RuntimeException();
|
||||||
|
return outBuff;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### com/google/android/vending/licensing/LicenseChecker.java
|
||||||
|
```
|
||||||
|
@@ -29,8 +29,8 @@ import android.os.RemoteException;
|
||||||
|
import android.provider.Settings.Secure;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
-import com.android.vending.licensing.ILicenseResultListener;
|
||||||
|
-import com.android.vending.licensing.ILicensingService;
|
||||||
|
+import com.google.android.vending.licensing.ILicenseResultListener;
|
||||||
|
+import com.google.android.vending.licensing.ILicensingService;
|
||||||
|
import com.google.android.vending.licensing.util.Base64;
|
||||||
|
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||||
|
```
|
||||||
|
```
|
||||||
|
@@ -287,13 +287,15 @@ public class LicenseChecker implements ServiceConnection {
|
||||||
|
if (logResponse) {
|
||||||
|
- String android_id = Secure.getString(mContext.getContentResolver(),
|
||||||
|
- Secure.ANDROID_ID);
|
||||||
|
+ String android_id = Secure.ANDROID_ID;
|
||||||
|
Date date = new Date();
|
||||||
|
```
|
@ -1,6 +1,5 @@
|
|||||||
#Sat Jul 29 16:10:03 ICT 2017
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
|
|
||||||
|
100
platform/android/java/gradlew
vendored
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
@ -6,42 +6,6 @@
|
|||||||
##
|
##
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS=""
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=`basename "$0"`
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD="maximum"
|
|
||||||
|
|
||||||
warn ( ) {
|
|
||||||
echo "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die ( ) {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN* )
|
|
||||||
cygwin=true
|
|
||||||
;;
|
|
||||||
Darwin* )
|
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
PRG="$0"
|
||||||
@ -60,6 +24,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
|
|||||||
APP_HOME="`pwd -P`"
|
APP_HOME="`pwd -P`"
|
||||||
cd "$SAVED" >/dev/null
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@ -85,7 +89,7 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# 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`
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
if [ $? -eq 0 ] ; then
|
if [ $? -eq 0 ] ; then
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
@ -150,11 +154,19 @@ if $cygwin ; then
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
# Escape application args
|
||||||
function splitJvmOpts() {
|
save () {
|
||||||
JVM_OPTS=("$@")
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
}
|
}
|
||||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
APP_ARGS=$(save "$@")
|
||||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
|
||||||
|
|
||||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
|
14
platform/android/java/gradlew.bat
vendored
@ -8,14 +8,14 @@
|
|||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
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
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
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
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
@ -46,10 +46,9 @@ echo location of your Java installation.
|
|||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
:init
|
||||||
@rem Get command-line arguments, handling Windowz variants
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
|
||||||
|
|
||||||
:win9xME_args
|
:win9xME_args
|
||||||
@rem Slurp the command line arguments.
|
@rem Slurp the command line arguments.
|
||||||
@ -60,11 +59,6 @@ set _SKIP=2
|
|||||||
if "x%~1" == "x" goto execute
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
set CMD_LINE_ARGS=%*
|
||||||
goto execute
|
|
||||||
|
|
||||||
:4NT_args
|
|
||||||
@rem Get arguments from the 4NT Shell from JP Software
|
|
||||||
set CMD_LINE_ARGS=%$
|
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 127 B After Width: | Height: | Size: 718 B |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 2.8 KiB |
@ -15,7 +15,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:layout_marginLeft="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
@ -23,12 +23,11 @@
|
|||||||
android:id="@+id/downloaderDashboard"
|
android:id="@+id/downloaderDashboard"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/statusText"
|
|
||||||
android:orientation="vertical" >
|
android:orientation="vertical" >
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" >
|
android:layout_weight="1" >
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -36,18 +35,15 @@
|
|||||||
style="@android:style/TextAppearance.Small"
|
style="@android:style/TextAppearance.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_marginLeft="5dp"
|
android:layout_marginStart="5dp" />
|
||||||
android:text="0MB / 0MB" >
|
|
||||||
</TextView>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/progressAsPercentage"
|
android:id="@+id/progressAsPercentage"
|
||||||
style="@android:style/TextAppearance.Small"
|
style="@android:style/TextAppearance.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignRight="@+id/progressBar"
|
android:layout_alignEnd="@+id/progressBar" />
|
||||||
android:text="0%" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
@ -58,24 +54,23 @@
|
|||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:layout_marginLeft="10dp"
|
android:layout_marginLeft="10dp"
|
||||||
android:layout_marginRight="10dp"
|
android:layout_marginRight="10dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp" />
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/progressAverageSpeed"
|
android:id="@+id/progressAverageSpeed"
|
||||||
style="@android:style/TextAppearance.Small"
|
style="@android:style/TextAppearance.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_below="@+id/progressBar"
|
android:layout_below="@+id/progressBar"
|
||||||
android:layout_marginLeft="5dp" />
|
android:layout_marginStart="5dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/progressTimeRemaining"
|
android:id="@+id/progressTimeRemaining"
|
||||||
style="@android:style/TextAppearance.Small"
|
style="@android:style/TextAppearance.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignRight="@+id/progressBar"
|
android:layout_alignEnd="@+id/progressBar"
|
||||||
android:layout_below="@+id/progressBar" />
|
android:layout_below="@+id/progressBar" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
@ -85,20 +80,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal" >
|
android:orientation="horizontal" >
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/pauseButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:layout_marginLeft="10dp"
|
|
||||||
android:layout_marginRight="5dp"
|
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
android:layout_weight="0"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:minWidth="94dp"
|
|
||||||
android:text="@string/text_button_pause" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/cancelButton"
|
android:id="@+id/cancelButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -112,7 +93,23 @@
|
|||||||
android:minHeight="40dp"
|
android:minHeight="40dp"
|
||||||
android:minWidth="94dp"
|
android:minWidth="94dp"
|
||||||
android:text="@string/text_button_cancel"
|
android:text="@string/text_button_cancel"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
style="?android:attr/buttonBarButtonStyle" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/pauseButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:minWidth="94dp"
|
||||||
|
android:text="@string/text_button_pause"
|
||||||
|
style="?android:attr/buttonBarButtonStyle" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -151,7 +148,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp"
|
||||||
android:text="@string/text_button_resume_cellular" />
|
android:text="@string/text_button_resume_cellular"
|
||||||
|
style="?android:attr/buttonBarButtonStyle" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/wifiSettingsButton"
|
android:id="@+id/wifiSettingsButton"
|
||||||
@ -159,7 +157,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp"
|
||||||
android:text="@string/text_button_wifi_settings" />
|
android:text="@string/text_button_wifi_settings"
|
||||||
|
style="?android:attr/buttonBarButtonStyle" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<LinearLayout android:layout_width="match_parent"
|
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android">
|
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_width="fill_parent"
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentTop="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
|
<TextView
|
||||||
android:id="@+id/progress_text"
|
android:id="@+id/progress_text"
|
||||||
style="@style/NotificationText"
|
style="@style/NotificationText"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
@ -56,15 +58,16 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingEnd="8dp"
|
||||||
android:paddingBottom="8dp" >
|
android:paddingBottom="8dp"
|
||||||
|
tools:ignore="RtlSymmetry">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
style="@style/NotificationTitle"
|
style="@style/NotificationTitle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentStart="true"
|
||||||
android:singleLine="true"/>
|
android:singleLine="true"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -72,8 +75,9 @@
|
|||||||
style="@style/NotificationText"
|
style="@style/NotificationText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:singleLine="true"/>
|
android:singleLine="true"
|
||||||
|
tools:ignore="RelativeOverlap" />
|
||||||
<!-- Only one of progress_bar and paused_text will be visible. -->
|
<!-- Only one of progress_bar and paused_text will be visible. -->
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
@ -87,7 +91,7 @@
|
|||||||
style="?android:attr/progressBarStyleHorizontal"
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingRight="25dp" />
|
android:paddingEnd="25dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/description"
|
android:id="@+id/description"
|
||||||
@ -95,7 +99,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:paddingRight="25dp"
|
android:paddingEnd="25dp"
|
||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<string name="notification_download_failed">다운로드 실패</string>
|
<string name="notification_download_failed">다운로드 실패</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="state_unknown">시작중...</string>
|
<string name="state_unknown">시작중…</string>
|
||||||
<string name="state_idle">다운로드 시작을 기다리는 중</string>
|
<string name="state_idle">다운로드 시작을 기다리는 중</string>
|
||||||
<string name="state_fetching_url">다운로드할 항목을 찾는 중</string>
|
<string name="state_fetching_url">다운로드할 항목을 찾는 중</string>
|
||||||
<string name="state_connecting">다운로드 서버에 연결 중</string>
|
<string name="state_connecting">다운로드 서버에 연결 중</string>
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="NotificationTextSecondary" parent="NotificationText">
|
|
||||||
<item name="android:textSize">12sp</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" />
|
|
||||||
<style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" />
|
|
||||||
</resources>
|
|
@ -30,7 +30,7 @@
|
|||||||
<string name="notification_download_failed">Download unsuccessful</string>
|
<string name="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_idle">Waiting for download to start</string>
|
||||||
<string name="state_fetching_url">Looking for resources to download</string>
|
<string name="state_fetching_url">Looking for resources to download</string>
|
||||||
<string name="state_connecting">Connecting to the download server</string>
|
<string name="state_connecting">Connecting to the download server</string>
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import com.google.android.vending.licensing.util.Base64;
|
|
||||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.spec.KeySpec;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An Obfuscator that uses AES to encrypt data.
|
|
||||||
*/
|
|
||||||
public class AESObfuscator implements Obfuscator {
|
|
||||||
private static final String UTF8 = "UTF-8";
|
|
||||||
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
|
|
||||||
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
|
|
||||||
private static final byte[] IV =
|
|
||||||
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
|
|
||||||
private static final String header = "com.android.vending.licensing.AESObfuscator-1|";
|
|
||||||
|
|
||||||
private Cipher mEncryptor;
|
|
||||||
private Cipher mDecryptor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param salt an array of random bytes to use for each (un)obfuscation
|
|
||||||
* @param applicationId application identifier, e.g. the package name
|
|
||||||
* @param deviceId device identifier. Use as many sources as possible to
|
|
||||||
* create this unique identifier.
|
|
||||||
*/
|
|
||||||
public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
|
|
||||||
try {
|
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
|
|
||||||
KeySpec keySpec =
|
|
||||||
new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
|
|
||||||
SecretKey tmp = factory.generateSecret(keySpec);
|
|
||||||
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
|
|
||||||
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
|
||||||
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
|
|
||||||
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
|
||||||
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
// This can't happen on a compatible Android device.
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String obfuscate(String original, String key) {
|
|
||||||
if (original == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Header is appended as an integrity check
|
|
||||||
return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String unobfuscate(String obfuscated, String key) throws ValidationException {
|
|
||||||
if (obfuscated == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
|
|
||||||
// Check for presence of header. This serves as a final integrity check, for cases
|
|
||||||
// where the block size is correct during decryption.
|
|
||||||
int headerIndex = result.indexOf(header+key);
|
|
||||||
if (headerIndex != 0) {
|
|
||||||
throw new ValidationException("Header not found (invalid data or key)" + ":" +
|
|
||||||
obfuscated);
|
|
||||||
}
|
|
||||||
return result.substring(header.length()+key.length(), result.length());
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (IllegalBlockSizeException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (BadPaddingException e) {
|
|
||||||
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("Invalid environment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,397 +0,0 @@
|
|||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.apache.http.NameValuePair;
|
|
||||||
import org.apache.http.client.utils.URLEncodedUtils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default policy. All policy decisions are based off of response data received
|
|
||||||
* from the licensing service. Specifically, the licensing server sends the
|
|
||||||
* following information: response validity period, error retry period, and
|
|
||||||
* error retry count.
|
|
||||||
* <p>
|
|
||||||
* These values will vary based on the the way the application is configured in
|
|
||||||
* the Android Market publishing console, such as whether the application is
|
|
||||||
* marked as free or is within its refund period, as well as how often an
|
|
||||||
* application is checking with the licensing service.
|
|
||||||
* <p>
|
|
||||||
* Developers who need more fine grained control over their application's
|
|
||||||
* licensing policy should implement a custom Policy.
|
|
||||||
*/
|
|
||||||
public class APKExpansionPolicy implements Policy {
|
|
||||||
|
|
||||||
private static final String TAG = "APKExpansionPolicy";
|
|
||||||
private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy";
|
|
||||||
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
|
||||||
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
|
||||||
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
|
||||||
private static final String PREF_MAX_RETRIES = "maxRetries";
|
|
||||||
private static final String PREF_RETRY_COUNT = "retryCount";
|
|
||||||
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
|
||||||
private static final String DEFAULT_RETRY_UNTIL = "0";
|
|
||||||
private static final String DEFAULT_MAX_RETRIES = "0";
|
|
||||||
private static final String DEFAULT_RETRY_COUNT = "0";
|
|
||||||
|
|
||||||
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
private long mValidityTimestamp;
|
|
||||||
private long mRetryUntil;
|
|
||||||
private long mMaxRetries;
|
|
||||||
private long mRetryCount;
|
|
||||||
private long mLastResponseTime = 0;
|
|
||||||
private int mLastResponse;
|
|
||||||
private PreferenceObfuscator mPreferences;
|
|
||||||
private Vector<String> mExpansionURLs = new Vector<String>();
|
|
||||||
private Vector<String> mExpansionFileNames = new Vector<String>();
|
|
||||||
private Vector<Long> mExpansionFileSizes = new Vector<Long>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The design of the protocol supports n files. Currently the market can
|
|
||||||
* only deliver two files. To accommodate this, we have these two constants,
|
|
||||||
* but the order is the only relevant thing here.
|
|
||||||
*/
|
|
||||||
public static final int MAIN_FILE_URL_INDEX = 0;
|
|
||||||
public static final int PATCH_FILE_URL_INDEX = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context The context for the current application
|
|
||||||
* @param obfuscator An obfuscator to be used with preferences.
|
|
||||||
*/
|
|
||||||
public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
|
|
||||||
// Import old values
|
|
||||||
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
|
||||||
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
|
||||||
mLastResponse = Integer.parseInt(
|
|
||||||
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
|
||||||
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
|
||||||
DEFAULT_VALIDITY_TIMESTAMP));
|
|
||||||
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
|
||||||
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
|
||||||
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We call this to guarantee that we fetch a fresh policy from the server.
|
|
||||||
* This is to be used if the URL is invalid.
|
|
||||||
*/
|
|
||||||
public void resetPolicy() {
|
|
||||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
|
|
||||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
|
||||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
|
||||||
setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
|
|
||||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
|
||||||
mPreferences.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a new response from the license server.
|
|
||||||
* <p>
|
|
||||||
* This data will be used for computing future policy decisions. The
|
|
||||||
* following parameters are processed:
|
|
||||||
* <ul>
|
|
||||||
* <li>VT: the timestamp that the client should consider the response valid
|
|
||||||
* until
|
|
||||||
* <li>GT: the timestamp that the client should ignore retry errors until
|
|
||||||
* <li>GR: the number of retry errors that the client should ignore
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @param response the result from validating the server response
|
|
||||||
* @param rawData the raw server response data
|
|
||||||
*/
|
|
||||||
public void processServerResponse(int response,
|
|
||||||
com.google.android.vending.licensing.ResponseData rawData) {
|
|
||||||
|
|
||||||
// Update retry counter
|
|
||||||
if (response != Policy.RETRY) {
|
|
||||||
setRetryCount(0);
|
|
||||||
} else {
|
|
||||||
setRetryCount(mRetryCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response == Policy.LICENSED) {
|
|
||||||
// Update server policy data
|
|
||||||
Map<String, String> extras = decodeExtras(rawData.extra);
|
|
||||||
mLastResponse = response;
|
|
||||||
setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
|
|
||||||
Set<String> keys = extras.keySet();
|
|
||||||
for (String key : keys) {
|
|
||||||
if (key.equals("VT")) {
|
|
||||||
setValidityTimestamp(extras.get(key));
|
|
||||||
} else if (key.equals("GT")) {
|
|
||||||
setRetryUntil(extras.get(key));
|
|
||||||
} else if (key.equals("GR")) {
|
|
||||||
setMaxRetries(extras.get(key));
|
|
||||||
} else if (key.startsWith("FILE_URL")) {
|
|
||||||
int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
|
|
||||||
setExpansionURL(index, extras.get(key));
|
|
||||||
} else if (key.startsWith("FILE_NAME")) {
|
|
||||||
int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
|
|
||||||
setExpansionFileName(index, extras.get(key));
|
|
||||||
} else if (key.startsWith("FILE_SIZE")) {
|
|
||||||
int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
|
|
||||||
setExpansionFileSize(index, Long.parseLong(extras.get(key)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (response == Policy.NOT_LICENSED) {
|
|
||||||
// Clear out stale policy data
|
|
||||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
|
||||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
|
||||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastResponse(response);
|
|
||||||
mPreferences.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last license response received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param l the response
|
|
||||||
*/
|
|
||||||
private void setLastResponse(int l) {
|
|
||||||
mLastResponseTime = System.currentTimeMillis();
|
|
||||||
mLastResponse = l;
|
|
||||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current retry count and add to preferences. You must manually
|
|
||||||
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param c the new retry count
|
|
||||||
*/
|
|
||||||
private void setRetryCount(long c) {
|
|
||||||
mRetryCount = c;
|
|
||||||
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRetryCount() {
|
|
||||||
return mRetryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last validity timestamp (VT) received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param validityTimestamp the VT string received
|
|
||||||
*/
|
|
||||||
private void setValidityTimestamp(String validityTimestamp) {
|
|
||||||
Long lValidityTimestamp;
|
|
||||||
try {
|
|
||||||
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parseable, expire in one minute.
|
|
||||||
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
|
||||||
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
|
||||||
validityTimestamp = Long.toString(lValidityTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
mValidityTimestamp = lValidityTimestamp;
|
|
||||||
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getValidityTimestamp() {
|
|
||||||
return mValidityTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the retry until timestamp (GT) received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param retryUntil the GT string received
|
|
||||||
*/
|
|
||||||
private void setRetryUntil(String retryUntil) {
|
|
||||||
Long lRetryUntil;
|
|
||||||
try {
|
|
||||||
lRetryUntil = Long.parseLong(retryUntil);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parseable, expire immediately
|
|
||||||
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
|
||||||
retryUntil = "0";
|
|
||||||
lRetryUntil = 0l;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRetryUntil = lRetryUntil;
|
|
||||||
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRetryUntil() {
|
|
||||||
return mRetryUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the max retries value (GR) as received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param maxRetries the GR string received
|
|
||||||
*/
|
|
||||||
private void setMaxRetries(String maxRetries) {
|
|
||||||
Long lMaxRetries;
|
|
||||||
try {
|
|
||||||
lMaxRetries = Long.parseLong(maxRetries);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parseable, expire immediately
|
|
||||||
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
|
||||||
maxRetries = "0";
|
|
||||||
lMaxRetries = 0l;
|
|
||||||
}
|
|
||||||
|
|
||||||
mMaxRetries = lMaxRetries;
|
|
||||||
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getMaxRetries() {
|
|
||||||
return mMaxRetries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the count of expansion URLs. Since expansionURLs are not committed
|
|
||||||
* to preferences, this will return zero if there has been no LVL fetch
|
|
||||||
* in the current session.
|
|
||||||
*
|
|
||||||
* @return the number of expansion URLs. (0,1,2)
|
|
||||||
*/
|
|
||||||
public int getExpansionURLCount() {
|
|
||||||
return mExpansionURLs.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the expansion URL. Since these URLs are not committed to
|
|
||||||
* preferences, this will always return null if there has not been an LVL
|
|
||||||
* fetch in the current session.
|
|
||||||
*
|
|
||||||
* @param index the index of the URL to fetch. This value will be either
|
|
||||||
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
|
||||||
* @param URL the URL to set
|
|
||||||
*/
|
|
||||||
public String getExpansionURL(int index) {
|
|
||||||
if (index < mExpansionURLs.size()) {
|
|
||||||
return mExpansionURLs.elementAt(index);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the expansion URL. Expansion URL's are not committed to preferences,
|
|
||||||
* but are instead intended to be stored when the license response is
|
|
||||||
* processed by the front-end.
|
|
||||||
*
|
|
||||||
* @param index the index of the expansion URL. This value will be either
|
|
||||||
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
|
||||||
* @param URL the URL to set
|
|
||||||
*/
|
|
||||||
public void setExpansionURL(int index, String URL) {
|
|
||||||
if (index >= mExpansionURLs.size()) {
|
|
||||||
mExpansionURLs.setSize(index + 1);
|
|
||||||
}
|
|
||||||
mExpansionURLs.set(index, URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getExpansionFileName(int index) {
|
|
||||||
if (index < mExpansionFileNames.size()) {
|
|
||||||
return mExpansionFileNames.elementAt(index);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExpansionFileName(int index, String name) {
|
|
||||||
if (index >= mExpansionFileNames.size()) {
|
|
||||||
mExpansionFileNames.setSize(index + 1);
|
|
||||||
}
|
|
||||||
mExpansionFileNames.set(index, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getExpansionFileSize(int index) {
|
|
||||||
if (index < mExpansionFileSizes.size()) {
|
|
||||||
return mExpansionFileSizes.elementAt(index);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExpansionFileSize(int index, long size) {
|
|
||||||
if (index >= mExpansionFileSizes.size()) {
|
|
||||||
mExpansionFileSizes.setSize(index + 1);
|
|
||||||
}
|
|
||||||
mExpansionFileSizes.set(index, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc} This implementation allows access if either:<br>
|
|
||||||
* <ol>
|
|
||||||
* <li>a LICENSED response was received within the validity period
|
|
||||||
* <li>a RETRY response was received in the last minute, and we are under
|
|
||||||
* the RETRY count or in the RETRY period.
|
|
||||||
* </ol>
|
|
||||||
*/
|
|
||||||
public boolean allowAccess() {
|
|
||||||
long ts = System.currentTimeMillis();
|
|
||||||
if (mLastResponse == Policy.LICENSED) {
|
|
||||||
// Check if the LICENSED response occurred within the validity
|
|
||||||
// timeout.
|
|
||||||
if (ts <= mValidityTimestamp) {
|
|
||||||
// Cached LICENSED response is still valid.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (mLastResponse == Policy.RETRY &&
|
|
||||||
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
|
||||||
// Only allow access if we are within the retry period or we haven't
|
|
||||||
// used up our
|
|
||||||
// max retries.
|
|
||||||
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> decodeExtras(String extras) {
|
|
||||||
Map<String, String> results = new HashMap<String, String>();
|
|
||||||
try {
|
|
||||||
URI rawExtras = new URI("?" + extras);
|
|
||||||
List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8");
|
|
||||||
for (NameValuePair item : extraList) {
|
|
||||||
String name = item.getName();
|
|
||||||
int i = 0;
|
|
||||||
while (results.containsKey(name)) {
|
|
||||||
name = item.getName() + ++i;
|
|
||||||
}
|
|
||||||
results.put(name, item.getValue());
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is auto-generated. DO NOT MODIFY.
|
|
||||||
* Original file: aidl/ILicenseResultListener.aidl
|
|
||||||
*/
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
import java.lang.String;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.IInterface;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.Parcel;
|
|
||||||
public interface ILicenseResultListener extends android.os.IInterface
|
|
||||||
{
|
|
||||||
/** Local-side IPC implementation stub class. */
|
|
||||||
public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener
|
|
||||||
{
|
|
||||||
private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
|
|
||||||
/** Construct the stub at attach it to the interface. */
|
|
||||||
public Stub()
|
|
||||||
{
|
|
||||||
this.attachInterface(this, DESCRIPTOR);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Cast an IBinder object into an ILicenseResultListener interface,
|
|
||||||
* generating a proxy if needed.
|
|
||||||
*/
|
|
||||||
public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj)
|
|
||||||
{
|
|
||||||
if ((obj==null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
|
|
||||||
if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
|
|
||||||
return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
|
|
||||||
}
|
|
||||||
return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
|
|
||||||
}
|
|
||||||
public android.os.IBinder asBinder()
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
|
||||||
{
|
|
||||||
switch (code)
|
|
||||||
{
|
|
||||||
case INTERFACE_TRANSACTION:
|
|
||||||
{
|
|
||||||
reply.writeString(DESCRIPTOR);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TRANSACTION_verifyLicense:
|
|
||||||
{
|
|
||||||
data.enforceInterface(DESCRIPTOR);
|
|
||||||
int _arg0;
|
|
||||||
_arg0 = data.readInt();
|
|
||||||
java.lang.String _arg1;
|
|
||||||
_arg1 = data.readString();
|
|
||||||
java.lang.String _arg2;
|
|
||||||
_arg2 = data.readString();
|
|
||||||
this.verifyLicense(_arg0, _arg1, _arg2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onTransact(code, data, reply, flags);
|
|
||||||
}
|
|
||||||
private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener
|
|
||||||
{
|
|
||||||
private android.os.IBinder mRemote;
|
|
||||||
Proxy(android.os.IBinder remote)
|
|
||||||
{
|
|
||||||
mRemote = remote;
|
|
||||||
}
|
|
||||||
public android.os.IBinder asBinder()
|
|
||||||
{
|
|
||||||
return mRemote;
|
|
||||||
}
|
|
||||||
public java.lang.String getInterfaceDescriptor()
|
|
||||||
{
|
|
||||||
return DESCRIPTOR;
|
|
||||||
}
|
|
||||||
public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException
|
|
||||||
{
|
|
||||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
|
||||||
try {
|
|
||||||
_data.writeInterfaceToken(DESCRIPTOR);
|
|
||||||
_data.writeInt(responseCode);
|
|
||||||
_data.writeString(signedData);
|
|
||||||
_data.writeString(signature);
|
|
||||||
mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_data.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
|
|
||||||
}
|
|
||||||
public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is auto-generated. DO NOT MODIFY.
|
|
||||||
* Original file: aidl/ILicensingService.aidl
|
|
||||||
*/
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
import java.lang.String;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.IInterface;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.Parcel;
|
|
||||||
public interface ILicensingService extends android.os.IInterface
|
|
||||||
{
|
|
||||||
/** Local-side IPC implementation stub class. */
|
|
||||||
public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService
|
|
||||||
{
|
|
||||||
private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
|
|
||||||
/** Construct the stub at attach it to the interface. */
|
|
||||||
public Stub()
|
|
||||||
{
|
|
||||||
this.attachInterface(this, DESCRIPTOR);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Cast an IBinder object into an ILicensingService interface,
|
|
||||||
* generating a proxy if needed.
|
|
||||||
*/
|
|
||||||
public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj)
|
|
||||||
{
|
|
||||||
if ((obj==null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
|
|
||||||
if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) {
|
|
||||||
return ((com.google.android.vending.licensing.ILicensingService)iin);
|
|
||||||
}
|
|
||||||
return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
|
|
||||||
}
|
|
||||||
public android.os.IBinder asBinder()
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
|
||||||
{
|
|
||||||
switch (code)
|
|
||||||
{
|
|
||||||
case INTERFACE_TRANSACTION:
|
|
||||||
{
|
|
||||||
reply.writeString(DESCRIPTOR);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case TRANSACTION_checkLicense:
|
|
||||||
{
|
|
||||||
data.enforceInterface(DESCRIPTOR);
|
|
||||||
long _arg0;
|
|
||||||
_arg0 = data.readLong();
|
|
||||||
java.lang.String _arg1;
|
|
||||||
_arg1 = data.readString();
|
|
||||||
com.google.android.vending.licensing.ILicenseResultListener _arg2;
|
|
||||||
_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
|
|
||||||
this.checkLicense(_arg0, _arg1, _arg2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onTransact(code, data, reply, flags);
|
|
||||||
}
|
|
||||||
private static class Proxy implements com.google.android.vending.licensing.ILicensingService
|
|
||||||
{
|
|
||||||
private android.os.IBinder mRemote;
|
|
||||||
Proxy(android.os.IBinder remote)
|
|
||||||
{
|
|
||||||
mRemote = remote;
|
|
||||||
}
|
|
||||||
public android.os.IBinder asBinder()
|
|
||||||
{
|
|
||||||
return mRemote;
|
|
||||||
}
|
|
||||||
public java.lang.String getInterfaceDescriptor()
|
|
||||||
{
|
|
||||||
return DESCRIPTOR;
|
|
||||||
}
|
|
||||||
public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException
|
|
||||||
{
|
|
||||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
|
||||||
try {
|
|
||||||
_data.writeInterfaceToken(DESCRIPTOR);
|
|
||||||
_data.writeLong(nonce);
|
|
||||||
_data.writeString(packageName);
|
|
||||||
_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null)));
|
|
||||||
mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_data.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
|
|
||||||
}
|
|
||||||
public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
|
|
||||||
}
|
|
@ -1,351 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import com.google.android.vending.licensing.util.Base64;
|
|
||||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.provider.Settings.Secure;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client library for Android Market license verifications.
|
|
||||||
* <p>
|
|
||||||
* The LicenseChecker is configured via a {@link Policy} which contains the
|
|
||||||
* logic to determine whether a user should have access to the application. For
|
|
||||||
* example, the Policy can define a threshold for allowable number of server or
|
|
||||||
* client failures before the library reports the user as not having access.
|
|
||||||
* <p>
|
|
||||||
* Must also provide the Base64-encoded RSA public key associated with your
|
|
||||||
* developer account. The public key is obtainable from the publisher site.
|
|
||||||
*/
|
|
||||||
public class LicenseChecker implements ServiceConnection {
|
|
||||||
private static final String TAG = "LicenseChecker";
|
|
||||||
|
|
||||||
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
|
||||||
|
|
||||||
// Timeout value (in milliseconds) for calls to service.
|
|
||||||
private static final int TIMEOUT_MS = 10 * 1000;
|
|
||||||
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
|
||||||
private static final boolean DEBUG_LICENSE_ERROR = false;
|
|
||||||
|
|
||||||
private ILicensingService mService;
|
|
||||||
|
|
||||||
private PublicKey mPublicKey;
|
|
||||||
private final Context mContext;
|
|
||||||
private final Policy mPolicy;
|
|
||||||
/**
|
|
||||||
* A handler for running tasks on a background thread. We don't want license
|
|
||||||
* processing to block the UI thread.
|
|
||||||
*/
|
|
||||||
private Handler mHandler;
|
|
||||||
private final String mPackageName;
|
|
||||||
private final String mVersionCode;
|
|
||||||
private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
|
|
||||||
private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context a Context
|
|
||||||
* @param policy implementation of Policy
|
|
||||||
* @param encodedPublicKey Base64-encoded RSA public key
|
|
||||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
|
||||||
*/
|
|
||||||
public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
|
|
||||||
mContext = context;
|
|
||||||
mPolicy = policy;
|
|
||||||
mPublicKey = generatePublicKey(encodedPublicKey);
|
|
||||||
mPackageName = mContext.getPackageName();
|
|
||||||
mVersionCode = getVersionCode(context, mPackageName);
|
|
||||||
HandlerThread handlerThread = new HandlerThread("background thread");
|
|
||||||
handlerThread.start();
|
|
||||||
mHandler = new Handler(handlerThread.getLooper());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a PublicKey instance from a string containing the
|
|
||||||
* Base64-encoded public key.
|
|
||||||
*
|
|
||||||
* @param encodedPublicKey Base64-encoded public key
|
|
||||||
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
|
||||||
*/
|
|
||||||
private static PublicKey generatePublicKey(String encodedPublicKey) {
|
|
||||||
try {
|
|
||||||
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
|
||||||
|
|
||||||
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// This won't happen in an Android-compatible environment.
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Could not decode from Base64.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
Log.e(TAG, "Invalid key specification.");
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the user should have access to the app. Binds the service if necessary.
|
|
||||||
* <p>
|
|
||||||
* NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security,
|
|
||||||
* we recommend obfuscating the string that is passed into bindService using another method
|
|
||||||
* of your own devising.
|
|
||||||
* <p>
|
|
||||||
* source string: "com.android.vending.licensing.ILicensingService"
|
|
||||||
* <p>
|
|
||||||
* @param callback
|
|
||||||
*/
|
|
||||||
public synchronized void checkAccess(LicenseCheckerCallback callback) {
|
|
||||||
// If we have a valid recent LICENSED response, we can skip asking
|
|
||||||
// Market.
|
|
||||||
if (mPolicy.allowAccess()) {
|
|
||||||
Log.i(TAG, "Using cached license response");
|
|
||||||
callback.allow(Policy.LICENSED);
|
|
||||||
} else {
|
|
||||||
LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
|
|
||||||
callback, generateNonce(), mPackageName, mVersionCode);
|
|
||||||
|
|
||||||
if (mService == null) {
|
|
||||||
Log.i(TAG, "Binding to licensing service.");
|
|
||||||
try {
|
|
||||||
Intent serviceIntent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")));
|
|
||||||
serviceIntent.setPackage("com.android.vending");
|
|
||||||
boolean bindResult = mContext
|
|
||||||
.bindService(
|
|
||||||
serviceIntent,
|
|
||||||
this, // ServiceConnection.
|
|
||||||
Context.BIND_AUTO_CREATE);
|
|
||||||
|
|
||||||
if (bindResult) {
|
|
||||||
mPendingChecks.offer(validator);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Could not bind to service.");
|
|
||||||
handleServiceConnectionError(validator);
|
|
||||||
}
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mPendingChecks.offer(validator);
|
|
||||||
runChecks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runChecks() {
|
|
||||||
LicenseValidator validator;
|
|
||||||
while ((validator = mPendingChecks.poll()) != null) {
|
|
||||||
try {
|
|
||||||
Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
|
|
||||||
mService.checkLicense(
|
|
||||||
validator.getNonce(), validator.getPackageName(),
|
|
||||||
new ResultListener(validator));
|
|
||||||
mChecksInProgress.add(validator);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
Log.w(TAG, "RemoteException in checkLicense call.", e);
|
|
||||||
handleServiceConnectionError(validator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void finishCheck(LicenseValidator validator) {
|
|
||||||
mChecksInProgress.remove(validator);
|
|
||||||
if (mChecksInProgress.isEmpty()) {
|
|
||||||
cleanupService();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ResultListener extends ILicenseResultListener.Stub {
|
|
||||||
private final LicenseValidator mValidator;
|
|
||||||
private Runnable mOnTimeout;
|
|
||||||
|
|
||||||
public ResultListener(LicenseValidator validator) {
|
|
||||||
mValidator = validator;
|
|
||||||
mOnTimeout = new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
Log.i(TAG, "Check timed out.");
|
|
||||||
handleServiceConnectionError(mValidator);
|
|
||||||
finishCheck(mValidator);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
startTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
|
||||||
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
|
||||||
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
|
||||||
|
|
||||||
// Runs in IPC thread pool. Post it to the Handler, so we can guarantee
|
|
||||||
// either this or the timeout runs.
|
|
||||||
public void verifyLicense(final int responseCode, final String signedData,
|
|
||||||
final String signature) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
Log.i(TAG, "Received response.");
|
|
||||||
// Make sure it hasn't already timed out.
|
|
||||||
if (mChecksInProgress.contains(mValidator)) {
|
|
||||||
clearTimeout();
|
|
||||||
mValidator.verify(mPublicKey, responseCode, signedData, signature);
|
|
||||||
finishCheck(mValidator);
|
|
||||||
}
|
|
||||||
if (DEBUG_LICENSE_ERROR) {
|
|
||||||
boolean logResponse;
|
|
||||||
String stringError = null;
|
|
||||||
switch (responseCode) {
|
|
||||||
case ERROR_CONTACTING_SERVER:
|
|
||||||
logResponse = true;
|
|
||||||
stringError = "ERROR_CONTACTING_SERVER";
|
|
||||||
break;
|
|
||||||
case ERROR_INVALID_PACKAGE_NAME:
|
|
||||||
logResponse = true;
|
|
||||||
stringError = "ERROR_INVALID_PACKAGE_NAME";
|
|
||||||
break;
|
|
||||||
case ERROR_NON_MATCHING_UID:
|
|
||||||
logResponse = true;
|
|
||||||
stringError = "ERROR_NON_MATCHING_UID";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logResponse = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logResponse) {
|
|
||||||
String android_id = Secure.getString(mContext.getContentResolver(),
|
|
||||||
Secure.ANDROID_ID);
|
|
||||||
Date date = new Date();
|
|
||||||
Log.d(TAG, "Server Failure: " + stringError);
|
|
||||||
Log.d(TAG, "Android ID: " + android_id);
|
|
||||||
Log.d(TAG, "Time: " + date.toGMTString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startTimeout() {
|
|
||||||
Log.i(TAG, "Start monitoring timeout.");
|
|
||||||
mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearTimeout() {
|
|
||||||
Log.i(TAG, "Clearing timeout.");
|
|
||||||
mHandler.removeCallbacks(mOnTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
mService = ILicensingService.Stub.asInterface(service);
|
|
||||||
runChecks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void onServiceDisconnected(ComponentName name) {
|
|
||||||
// Called when the connection with the service has been
|
|
||||||
// unexpectedly disconnected. That is, Market crashed.
|
|
||||||
// If there are any checks in progress, the timeouts will handle them.
|
|
||||||
Log.w(TAG, "Service unexpectedly disconnected.");
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates policy response for service connection errors, as a result of
|
|
||||||
* disconnections or timeouts.
|
|
||||||
*/
|
|
||||||
private synchronized void handleServiceConnectionError(LicenseValidator validator) {
|
|
||||||
mPolicy.processServerResponse(Policy.RETRY, null);
|
|
||||||
|
|
||||||
if (mPolicy.allowAccess()) {
|
|
||||||
validator.getCallback().allow(Policy.RETRY);
|
|
||||||
} else {
|
|
||||||
validator.getCallback().dontAllow(Policy.RETRY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Unbinds service if necessary and removes reference to it. */
|
|
||||||
private void cleanupService() {
|
|
||||||
if (mService != null) {
|
|
||||||
try {
|
|
||||||
mContext.unbindService(this);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// Somehow we've already been unbound. This is a non-fatal
|
|
||||||
// error.
|
|
||||||
Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
|
|
||||||
}
|
|
||||||
mService = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inform the library that the context is about to be destroyed, so that any
|
|
||||||
* open connections can be cleaned up.
|
|
||||||
* <p>
|
|
||||||
* Failure to call this method can result in a crash under certain
|
|
||||||
* circumstances, such as during screen rotation if an Activity requests the
|
|
||||||
* license check or when the user exits the application.
|
|
||||||
*/
|
|
||||||
public synchronized void onDestroy() {
|
|
||||||
cleanupService();
|
|
||||||
mHandler.getLooper().quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generates a nonce (number used once). */
|
|
||||||
private int generateNonce() {
|
|
||||||
return RANDOM.nextInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get version code for the application package name.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param packageName application package name
|
|
||||||
* @return the version code or empty string if package not found
|
|
||||||
*/
|
|
||||||
private static String getVersionCode(Context context, String packageName) {
|
|
||||||
try {
|
|
||||||
return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0).
|
|
||||||
versionCode);
|
|
||||||
} catch (NameNotFoundException e) {
|
|
||||||
Log.e(TAG, "Package not found. could not get version code.");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import com.google.android.vending.licensing.util.Base64;
|
|
||||||
import com.google.android.vending.licensing.util.Base64DecoderException;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains data related to a licensing request and methods to verify
|
|
||||||
* and process the response.
|
|
||||||
*/
|
|
||||||
class LicenseValidator {
|
|
||||||
private static final String TAG = "LicenseValidator";
|
|
||||||
|
|
||||||
// Server response codes.
|
|
||||||
private static final int LICENSED = 0x0;
|
|
||||||
private static final int NOT_LICENSED = 0x1;
|
|
||||||
private static final int LICENSED_OLD_KEY = 0x2;
|
|
||||||
private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
|
|
||||||
private static final int ERROR_SERVER_FAILURE = 0x4;
|
|
||||||
private static final int ERROR_OVER_QUOTA = 0x5;
|
|
||||||
|
|
||||||
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
|
||||||
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
|
||||||
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
|
||||||
|
|
||||||
private final Policy mPolicy;
|
|
||||||
private final LicenseCheckerCallback mCallback;
|
|
||||||
private final int mNonce;
|
|
||||||
private final String mPackageName;
|
|
||||||
private final String mVersionCode;
|
|
||||||
private final DeviceLimiter mDeviceLimiter;
|
|
||||||
|
|
||||||
LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
|
|
||||||
int nonce, String packageName, String versionCode) {
|
|
||||||
mPolicy = policy;
|
|
||||||
mDeviceLimiter = deviceLimiter;
|
|
||||||
mCallback = callback;
|
|
||||||
mNonce = nonce;
|
|
||||||
mPackageName = packageName;
|
|
||||||
mVersionCode = versionCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LicenseCheckerCallback getCallback() {
|
|
||||||
return mCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNonce() {
|
|
||||||
return mNonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPackageName() {
|
|
||||||
return mPackageName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies the response from server and calls appropriate callback method.
|
|
||||||
*
|
|
||||||
* @param publicKey public key associated with the developer account
|
|
||||||
* @param responseCode server response code
|
|
||||||
* @param signedData signed data from server
|
|
||||||
* @param signature server signature
|
|
||||||
*/
|
|
||||||
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
|
|
||||||
String userId = null;
|
|
||||||
// Skip signature check for unsuccessful requests
|
|
||||||
ResponseData data = null;
|
|
||||||
if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
|
|
||||||
responseCode == LICENSED_OLD_KEY) {
|
|
||||||
// Verify signature.
|
|
||||||
try {
|
|
||||||
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
|
||||||
sig.initVerify(publicKey);
|
|
||||||
sig.update(signedData.getBytes());
|
|
||||||
|
|
||||||
if (!sig.verify(Base64.decode(signature))) {
|
|
||||||
Log.e(TAG, "Signature verification failed.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// This can't happen on an Android compatible device.
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
|
|
||||||
return;
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (Base64DecoderException e) {
|
|
||||||
Log.e(TAG, "Could not Base64-decode signature.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and validate response.
|
|
||||||
try {
|
|
||||||
data = ResponseData.parse(signedData);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Log.e(TAG, "Could not parse response.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.responseCode != responseCode) {
|
|
||||||
Log.e(TAG, "Response codes don't match.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.nonce != mNonce) {
|
|
||||||
Log.e(TAG, "Nonce doesn't match.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.packageName.equals(mPackageName)) {
|
|
||||||
Log.e(TAG, "Package name doesn't match.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.versionCode.equals(mVersionCode)) {
|
|
||||||
Log.e(TAG, "Version codes don't match.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application-specific user identifier.
|
|
||||||
userId = data.userId;
|
|
||||||
if (TextUtils.isEmpty(userId)) {
|
|
||||||
Log.e(TAG, "User identifier is empty.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (responseCode) {
|
|
||||||
case LICENSED:
|
|
||||||
case LICENSED_OLD_KEY:
|
|
||||||
int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
|
|
||||||
handleResponse(limiterResponse, data);
|
|
||||||
break;
|
|
||||||
case NOT_LICENSED:
|
|
||||||
handleResponse(Policy.NOT_LICENSED, data);
|
|
||||||
break;
|
|
||||||
case ERROR_CONTACTING_SERVER:
|
|
||||||
Log.w(TAG, "Error contacting licensing server.");
|
|
||||||
handleResponse(Policy.RETRY, data);
|
|
||||||
break;
|
|
||||||
case ERROR_SERVER_FAILURE:
|
|
||||||
Log.w(TAG, "An error has occurred on the licensing server.");
|
|
||||||
handleResponse(Policy.RETRY, data);
|
|
||||||
break;
|
|
||||||
case ERROR_OVER_QUOTA:
|
|
||||||
Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
|
|
||||||
handleResponse(Policy.RETRY, data);
|
|
||||||
break;
|
|
||||||
case ERROR_INVALID_PACKAGE_NAME:
|
|
||||||
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
|
|
||||||
break;
|
|
||||||
case ERROR_NON_MATCHING_UID:
|
|
||||||
handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
|
|
||||||
break;
|
|
||||||
case ERROR_NOT_MARKET_MANAGED:
|
|
||||||
handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.e(TAG, "Unknown response code for license check.");
|
|
||||||
handleInvalidResponse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confers with policy and calls appropriate callback method.
|
|
||||||
*
|
|
||||||
* @param response
|
|
||||||
* @param rawData
|
|
||||||
*/
|
|
||||||
private void handleResponse(int response, ResponseData rawData) {
|
|
||||||
// Update policy data and increment retry counter (if needed)
|
|
||||||
mPolicy.processServerResponse(response, rawData);
|
|
||||||
|
|
||||||
// Given everything we know, including cached data, ask the policy if we should grant
|
|
||||||
// access.
|
|
||||||
if (mPolicy.allowAccess()) {
|
|
||||||
mCallback.allow(response);
|
|
||||||
} else {
|
|
||||||
mCallback.dontAllow(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleApplicationError(int code) {
|
|
||||||
mCallback.applicationError(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInvalidResponse() {
|
|
||||||
mCallback.dontAllow(Policy.NOT_LICENSED);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An wrapper for SharedPreferences that transparently performs data obfuscation.
|
|
||||||
*/
|
|
||||||
public class PreferenceObfuscator {
|
|
||||||
|
|
||||||
private static final String TAG = "PreferenceObfuscator";
|
|
||||||
|
|
||||||
private final SharedPreferences mPreferences;
|
|
||||||
private final Obfuscator mObfuscator;
|
|
||||||
private SharedPreferences.Editor mEditor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param sp A SharedPreferences instance provided by the system.
|
|
||||||
* @param o The Obfuscator to use when reading or writing data.
|
|
||||||
*/
|
|
||||||
public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
|
|
||||||
mPreferences = sp;
|
|
||||||
mObfuscator = o;
|
|
||||||
mEditor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putString(String key, String value) {
|
|
||||||
if (mEditor == null) {
|
|
||||||
mEditor = mPreferences.edit();
|
|
||||||
}
|
|
||||||
String obfuscatedValue = mObfuscator.obfuscate(value, key);
|
|
||||||
mEditor.putString(key, obfuscatedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getString(String key, String defValue) {
|
|
||||||
String result;
|
|
||||||
String value = mPreferences.getString(key, null);
|
|
||||||
if (value != null) {
|
|
||||||
try {
|
|
||||||
result = mObfuscator.unobfuscate(value, key);
|
|
||||||
} catch (ValidationException e) {
|
|
||||||
// Unable to unobfuscate, data corrupt or tampered
|
|
||||||
Log.w(TAG, "Validation error while reading preference: " + key);
|
|
||||||
result = defValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Preference not found
|
|
||||||
result = defValue;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void commit() {
|
|
||||||
if (mEditor != null) {
|
|
||||||
mEditor.commit();
|
|
||||||
mEditor = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ResponseData from licensing server.
|
|
||||||
*/
|
|
||||||
public class ResponseData {
|
|
||||||
|
|
||||||
public int responseCode;
|
|
||||||
public int nonce;
|
|
||||||
public String packageName;
|
|
||||||
public String versionCode;
|
|
||||||
public String userId;
|
|
||||||
public long timestamp;
|
|
||||||
/** Response-specific data. */
|
|
||||||
public String extra;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses response string into ResponseData.
|
|
||||||
*
|
|
||||||
* @param responseData response data string
|
|
||||||
* @throws IllegalArgumentException upon parsing error
|
|
||||||
* @return ResponseData object
|
|
||||||
*/
|
|
||||||
public static ResponseData parse(String responseData) {
|
|
||||||
// Must parse out main response data and response-specific data.
|
|
||||||
int index = responseData.indexOf(':');
|
|
||||||
String mainData, extraData;
|
|
||||||
if ( -1 == index ) {
|
|
||||||
mainData = responseData;
|
|
||||||
extraData = "";
|
|
||||||
} else {
|
|
||||||
mainData = responseData.substring(0, index);
|
|
||||||
extraData = index >= responseData.length() ? "" : responseData.substring(index+1);
|
|
||||||
}
|
|
||||||
|
|
||||||
String [] fields = TextUtils.split(mainData, Pattern.quote("|"));
|
|
||||||
if (fields.length < 6) {
|
|
||||||
throw new IllegalArgumentException("Wrong number of fields.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseData data = new ResponseData();
|
|
||||||
data.extra = extraData;
|
|
||||||
data.responseCode = Integer.parseInt(fields[0]);
|
|
||||||
data.nonce = Integer.parseInt(fields[1]);
|
|
||||||
data.packageName = fields[2];
|
|
||||||
data.versionCode = fields[3];
|
|
||||||
// Application-specific user identifier.
|
|
||||||
data.userId = fields[4];
|
|
||||||
data.timestamp = Long.parseLong(fields[5]);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode,
|
|
||||||
userId, timestamp });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,276 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2010 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing;
|
|
||||||
|
|
||||||
import org.apache.http.NameValuePair;
|
|
||||||
import org.apache.http.client.utils.URLEncodedUtils;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default policy. All policy decisions are based off of response data received
|
|
||||||
* from the licensing service. Specifically, the licensing server sends the
|
|
||||||
* following information: response validity period, error retry period, and
|
|
||||||
* error retry count.
|
|
||||||
* <p>
|
|
||||||
* These values will vary based on the the way the application is configured in
|
|
||||||
* the Android Market publishing console, such as whether the application is
|
|
||||||
* marked as free or is within its refund period, as well as how often an
|
|
||||||
* application is checking with the licensing service.
|
|
||||||
* <p>
|
|
||||||
* Developers who need more fine grained control over their application's
|
|
||||||
* licensing policy should implement a custom Policy.
|
|
||||||
*/
|
|
||||||
public class ServerManagedPolicy implements Policy {
|
|
||||||
|
|
||||||
private static final String TAG = "ServerManagedPolicy";
|
|
||||||
private static final String PREFS_FILE = "com.android.vending.licensing.ServerManagedPolicy";
|
|
||||||
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
|
||||||
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
|
||||||
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
|
||||||
private static final String PREF_MAX_RETRIES = "maxRetries";
|
|
||||||
private static final String PREF_RETRY_COUNT = "retryCount";
|
|
||||||
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
|
||||||
private static final String DEFAULT_RETRY_UNTIL = "0";
|
|
||||||
private static final String DEFAULT_MAX_RETRIES = "0";
|
|
||||||
private static final String DEFAULT_RETRY_COUNT = "0";
|
|
||||||
|
|
||||||
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
private long mValidityTimestamp;
|
|
||||||
private long mRetryUntil;
|
|
||||||
private long mMaxRetries;
|
|
||||||
private long mRetryCount;
|
|
||||||
private long mLastResponseTime = 0;
|
|
||||||
private int mLastResponse;
|
|
||||||
private PreferenceObfuscator mPreferences;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context The context for the current application
|
|
||||||
* @param obfuscator An obfuscator to be used with preferences.
|
|
||||||
*/
|
|
||||||
public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
|
|
||||||
// Import old values
|
|
||||||
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
|
||||||
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
|
||||||
mLastResponse = Integer.parseInt(
|
|
||||||
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
|
||||||
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
|
||||||
DEFAULT_VALIDITY_TIMESTAMP));
|
|
||||||
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
|
||||||
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
|
||||||
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a new response from the license server.
|
|
||||||
* <p>
|
|
||||||
* This data will be used for computing future policy decisions. The
|
|
||||||
* following parameters are processed:
|
|
||||||
* <ul>
|
|
||||||
* <li>VT: the timestamp that the client should consider the response
|
|
||||||
* valid until
|
|
||||||
* <li>GT: the timestamp that the client should ignore retry errors until
|
|
||||||
* <li>GR: the number of retry errors that the client should ignore
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @param response the result from validating the server response
|
|
||||||
* @param rawData the raw server response data
|
|
||||||
*/
|
|
||||||
public void processServerResponse(int response, ResponseData rawData) {
|
|
||||||
|
|
||||||
// Update retry counter
|
|
||||||
if (response != Policy.RETRY) {
|
|
||||||
setRetryCount(0);
|
|
||||||
} else {
|
|
||||||
setRetryCount(mRetryCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response == Policy.LICENSED) {
|
|
||||||
// Update server policy data
|
|
||||||
Map<String, String> extras = decodeExtras(rawData.extra);
|
|
||||||
mLastResponse = response;
|
|
||||||
setValidityTimestamp(extras.get("VT"));
|
|
||||||
setRetryUntil(extras.get("GT"));
|
|
||||||
setMaxRetries(extras.get("GR"));
|
|
||||||
} else if (response == Policy.NOT_LICENSED) {
|
|
||||||
// Clear out stale policy data
|
|
||||||
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
|
||||||
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
|
||||||
setMaxRetries(DEFAULT_MAX_RETRIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastResponse(response);
|
|
||||||
mPreferences.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last license response received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param l the response
|
|
||||||
*/
|
|
||||||
private void setLastResponse(int l) {
|
|
||||||
mLastResponseTime = System.currentTimeMillis();
|
|
||||||
mLastResponse = l;
|
|
||||||
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current retry count and add to preferences. You must manually
|
|
||||||
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param c the new retry count
|
|
||||||
*/
|
|
||||||
private void setRetryCount(long c) {
|
|
||||||
mRetryCount = c;
|
|
||||||
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRetryCount() {
|
|
||||||
return mRetryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the last validity timestamp (VT) received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param validityTimestamp the VT string received
|
|
||||||
*/
|
|
||||||
private void setValidityTimestamp(String validityTimestamp) {
|
|
||||||
Long lValidityTimestamp;
|
|
||||||
try {
|
|
||||||
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parsable, expire in one minute.
|
|
||||||
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
|
||||||
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
|
||||||
validityTimestamp = Long.toString(lValidityTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
mValidityTimestamp = lValidityTimestamp;
|
|
||||||
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getValidityTimestamp() {
|
|
||||||
return mValidityTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the retry until timestamp (GT) received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param retryUntil the GT string received
|
|
||||||
*/
|
|
||||||
private void setRetryUntil(String retryUntil) {
|
|
||||||
Long lRetryUntil;
|
|
||||||
try {
|
|
||||||
lRetryUntil = Long.parseLong(retryUntil);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parsable, expire immediately
|
|
||||||
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
|
||||||
retryUntil = "0";
|
|
||||||
lRetryUntil = 0l;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRetryUntil = lRetryUntil;
|
|
||||||
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRetryUntil() {
|
|
||||||
return mRetryUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the max retries value (GR) as received from the server and add to
|
|
||||||
* preferences. You must manually call PreferenceObfuscator.commit() to
|
|
||||||
* commit these changes to disk.
|
|
||||||
*
|
|
||||||
* @param maxRetries the GR string received
|
|
||||||
*/
|
|
||||||
private void setMaxRetries(String maxRetries) {
|
|
||||||
Long lMaxRetries;
|
|
||||||
try {
|
|
||||||
lMaxRetries = Long.parseLong(maxRetries);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// No response or not parsable, expire immediately
|
|
||||||
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
|
||||||
maxRetries = "0";
|
|
||||||
lMaxRetries = 0l;
|
|
||||||
}
|
|
||||||
|
|
||||||
mMaxRetries = lMaxRetries;
|
|
||||||
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getMaxRetries() {
|
|
||||||
return mMaxRetries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*
|
|
||||||
* This implementation allows access if either:<br>
|
|
||||||
* <ol>
|
|
||||||
* <li>a LICENSED response was received within the validity period
|
|
||||||
* <li>a RETRY response was received in the last minute, and we are under
|
|
||||||
* the RETRY count or in the RETRY period.
|
|
||||||
* </ol>
|
|
||||||
*/
|
|
||||||
public boolean allowAccess() {
|
|
||||||
long ts = System.currentTimeMillis();
|
|
||||||
if (mLastResponse == Policy.LICENSED) {
|
|
||||||
// Check if the LICENSED response occurred within the validity timeout.
|
|
||||||
if (ts <= mValidityTimestamp) {
|
|
||||||
// Cached LICENSED response is still valid.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (mLastResponse == Policy.RETRY &&
|
|
||||||
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
|
||||||
// Only allow access if we are within the retry period or we haven't used up our
|
|
||||||
// max retries.
|
|
||||||
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> decodeExtras(String extras) {
|
|
||||||
Map<String, String> results = new HashMap<String, String>();
|
|
||||||
try {
|
|
||||||
URI rawExtras = new URI("?" + extras);
|
|
||||||
List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8");
|
|
||||||
for (NameValuePair item : extraList) {
|
|
||||||
results.put(item.getName(), item.getValue());
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,570 +0,0 @@
|
|||||||
// Portions copyright 2002, Google, Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.android.vending.licensing.util;
|
|
||||||
|
|
||||||
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
|
||||||
// Lots of extraneous features were removed.
|
|
||||||
/* The original code said:
|
|
||||||
* <p>
|
|
||||||
* I am placing this code in the Public Domain. Do with it as you will.
|
|
||||||
* This software comes with no guarantees or warranties but with
|
|
||||||
* plenty of well-wishing instead!
|
|
||||||
* Please visit
|
|
||||||
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
|
||||||
* periodically to check for updates or to contribute improvements.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @author Robert Harder
|
|
||||||
* @author rharder@usa.net
|
|
||||||
* @version 1.3
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base64 converter class. This code is not a full-blown MIME encoder;
|
|
||||||
* it simply converts binary data to base64 data and back.
|
|
||||||
*
|
|
||||||
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
|
||||||
* class.
|
|
||||||
*/
|
|
||||||
public class Base64 {
|
|
||||||
/** Specify encoding (value is {@code true}). */
|
|
||||||
public final static boolean ENCODE = true;
|
|
||||||
|
|
||||||
/** Specify decoding (value is {@code false}). */
|
|
||||||
public final static boolean DECODE = false;
|
|
||||||
|
|
||||||
/** The equals sign (=) as a byte. */
|
|
||||||
private final static byte EQUALS_SIGN = (byte) '=';
|
|
||||||
|
|
||||||
/** The new line character (\n) as a byte. */
|
|
||||||
private final static byte NEW_LINE = (byte) '\n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '+', (byte) '/'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 64 valid web safe Base64 values.
|
|
||||||
*/
|
|
||||||
private final static byte[] WEBSAFE_ALPHABET =
|
|
||||||
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
|
|
||||||
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
|
|
||||||
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
|
|
||||||
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
|
|
||||||
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
|
|
||||||
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
|
|
||||||
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
|
|
||||||
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
|
|
||||||
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
|
|
||||||
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
|
|
||||||
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
|
|
||||||
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
|
|
||||||
(byte) '9', (byte) '-', (byte) '_'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates a Base64 value to either its 6-bit reconstruction value
|
|
||||||
* or a negative number indicating some other meaning.
|
|
||||||
**/
|
|
||||||
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
|
||||||
62, // Plus sign at decimal 43
|
|
||||||
-9, -9, -9, // Decimal 44 - 46
|
|
||||||
63, // Slash at decimal 47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/** The web safe decodabet */
|
|
||||||
private final static byte[] WEBSAFE_DECODABET =
|
|
||||||
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
|
||||||
-5, -5, // Whitespace: Tab and Linefeed
|
|
||||||
-9, -9, // Decimal 11 - 12
|
|
||||||
-5, // Whitespace: Carriage Return
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
|
||||||
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
|
||||||
-5, // Whitespace: Space
|
|
||||||
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
|
||||||
62, // Dash '-' sign at decimal 45
|
|
||||||
-9, -9, // Decimal 46-47
|
|
||||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
|
||||||
-9, -9, -9, // Decimal 58 - 60
|
|
||||||
-1, // Equals sign at decimal 61
|
|
||||||
-9, -9, -9, // Decimal 62 - 64
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
|
||||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
|
||||||
-9, -9, -9, -9, // Decimal 91-94
|
|
||||||
63, // Underscore '_' at decimal 95
|
|
||||||
-9, // Decimal 96
|
|
||||||
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
|
||||||
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
|
||||||
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
|
||||||
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
|
||||||
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
|
||||||
};
|
|
||||||
|
|
||||||
// Indicates white space in encoding
|
|
||||||
private final static byte WHITE_SPACE_ENC = -5;
|
|
||||||
// Indicates equals sign in encoding
|
|
||||||
private final static byte EQUALS_SIGN_ENC = -1;
|
|
||||||
|
|
||||||
/** Defeats instantiation. */
|
|
||||||
private Base64() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ******** E N C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes up to three bytes of the array <var>source</var>
|
|
||||||
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* The actual number of significant bytes in your array is
|
|
||||||
* given by <var>numSigBytes</var>.
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param numSigBytes the number of significant bytes in your array
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @return the <var>destination</var> array
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static byte[] encode3to4(byte[] source, int srcOffset,
|
|
||||||
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
|
||||||
// 1 2 3
|
|
||||||
// 01234567890123456789012345678901 Bit position
|
|
||||||
// --------000000001111111122222222 Array position from threeBytes
|
|
||||||
// --------| || || || | Six bit groups to index alphabet
|
|
||||||
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
|
||||||
// 0x3f 0x3f 0x3f Additional AND
|
|
||||||
|
|
||||||
// Create buffer with zero-padding if there are only one or two
|
|
||||||
// significant bytes passed in the array.
|
|
||||||
// We have to shift left 24 in order to flush out the 1's that appear
|
|
||||||
// when Java treats a value as negative that is cast from a byte to an int.
|
|
||||||
int inBuff =
|
|
||||||
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
|
|
||||||
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
|
|
||||||
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
|
||||||
|
|
||||||
switch (numSigBytes) {
|
|
||||||
case 3:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
return destination;
|
|
||||||
case 2:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
case 1:
|
|
||||||
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
|
||||||
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
destination[destOffset + 2] = EQUALS_SIGN;
|
|
||||||
destination[destOffset + 3] = EQUALS_SIGN;
|
|
||||||
return destination;
|
|
||||||
default:
|
|
||||||
return destination;
|
|
||||||
} // end switch
|
|
||||||
} // end encode3to4
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
* Equivalent to calling
|
|
||||||
* {@code encodeBytes(source, 0, source.length)}
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source) {
|
|
||||||
return encode(source, 0, source.length, ALPHABET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into web safe Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
*/
|
|
||||||
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
|
||||||
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @param off Offset in array where conversion should begin
|
|
||||||
* @param len Length of data to convert
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @param doPadding is {@code true} to pad result with '=' chars
|
|
||||||
* if it does not fall on 3 byte boundaries
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
boolean doPadding) {
|
|
||||||
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
|
||||||
int outLen = outBuff.length;
|
|
||||||
|
|
||||||
// If doPadding is false, set length to truncate '='
|
|
||||||
// padding characters
|
|
||||||
while (doPadding == false && outLen > 0) {
|
|
||||||
if (outBuff[outLen - 1] != '=') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
outLen -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String(outBuff, 0, outLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into Base64 notation.
|
|
||||||
*
|
|
||||||
* @param source The data to convert
|
|
||||||
* @param off Offset in array where conversion should begin
|
|
||||||
* @param len Length of data to convert
|
|
||||||
* @param alphabet is the encoding alphabet
|
|
||||||
* @param maxLineLength maximum length of one line.
|
|
||||||
* @return the BASE64-encoded byte array
|
|
||||||
*/
|
|
||||||
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
|
||||||
int maxLineLength) {
|
|
||||||
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
|
||||||
int len43 = lenDiv3 * 4;
|
|
||||||
byte[] outBuff = new byte[len43 // Main 4:3
|
|
||||||
+ (len43 / maxLineLength)]; // New lines
|
|
||||||
|
|
||||||
int d = 0;
|
|
||||||
int e = 0;
|
|
||||||
int len2 = len - 2;
|
|
||||||
int lineLength = 0;
|
|
||||||
for (; d < len2; d += 3, e += 4) {
|
|
||||||
|
|
||||||
// The following block of code is the same as
|
|
||||||
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
|
||||||
// but inlined for faster encoding (~20% improvement)
|
|
||||||
int inBuff =
|
|
||||||
((source[d + off] << 24) >>> 8)
|
|
||||||
| ((source[d + 1 + off] << 24) >>> 16)
|
|
||||||
| ((source[d + 2 + off] << 24) >>> 24);
|
|
||||||
outBuff[e] = alphabet[(inBuff >>> 18)];
|
|
||||||
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
|
||||||
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
|
||||||
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
lineLength = 0;
|
|
||||||
} // end if: end of line
|
|
||||||
} // end for: each piece of array
|
|
||||||
|
|
||||||
if (d < len) {
|
|
||||||
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
|
||||||
|
|
||||||
lineLength += 4;
|
|
||||||
if (lineLength == maxLineLength) {
|
|
||||||
// Add a last newline
|
|
||||||
outBuff[e + 4] = NEW_LINE;
|
|
||||||
e++;
|
|
||||||
}
|
|
||||||
e += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert (e == outBuff.length);
|
|
||||||
return outBuff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ******** D E C O D I N G M E T H O D S ******** */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes four bytes from array <var>source</var>
|
|
||||||
* and writes the resulting bytes (up to three of them)
|
|
||||||
* to <var>destination</var>.
|
|
||||||
* The source and destination arrays can be manipulated
|
|
||||||
* anywhere along their length by specifying
|
|
||||||
* <var>srcOffset</var> and <var>destOffset</var>.
|
|
||||||
* This method does not check to make sure your arrays
|
|
||||||
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
|
||||||
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
|
||||||
* the <var>destination</var> array.
|
|
||||||
* This method returns the actual number of bytes that
|
|
||||||
* were converted from the Base64 encoding.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param source the array to convert
|
|
||||||
* @param srcOffset the index where conversion begins
|
|
||||||
* @param destination the array to hold the conversion
|
|
||||||
* @param destOffset the index where output will be put
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return the number of decoded bytes converted
|
|
||||||
* @since 1.3
|
|
||||||
*/
|
|
||||||
private static int decode4to3(byte[] source, int srcOffset,
|
|
||||||
byte[] destination, int destOffset, byte[] decodabet) {
|
|
||||||
// Example: Dk==
|
|
||||||
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
return 1;
|
|
||||||
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
|
||||||
// Example: DkL=
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >>> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >>> 8);
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
// Example: DkLE
|
|
||||||
int outBuff =
|
|
||||||
((decodabet[source[srcOffset]] << 24) >>> 6)
|
|
||||||
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
|
|
||||||
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
|
|
||||||
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
|
||||||
|
|
||||||
destination[destOffset] = (byte) (outBuff >> 16);
|
|
||||||
destination[destOffset + 1] = (byte) (outBuff >> 8);
|
|
||||||
destination[destOffset + 2] = (byte) (outBuff);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
} // end decodeToBytes
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from Base64 notation.
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
* @since 1.4
|
|
||||||
*/
|
|
||||||
public static byte[] decode(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decode(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes data from web safe Base64 notation.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param s the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
|
||||||
byte[] bytes = s.getBytes();
|
|
||||||
return decodeWebSafe(bytes, 0, bytes.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
|
||||||
return decode(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded data.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source the string to decode (decoded in default encoding)
|
|
||||||
* @return the decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decodeWebSafe(source, 0, source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @param off The offset of where to begin decoding
|
|
||||||
* @param len The length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
* @since 1.3
|
|
||||||
* @throws Base64DecoderException
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes web safe Base64 content in byte array format and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @param off The offset of where to begin decoding
|
|
||||||
* @param len The length of characters to decode
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
return decode(source, off, len, WEBSAFE_DECODABET);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 content using the supplied decodabet and returns
|
|
||||||
* the decoded byte array.
|
|
||||||
*
|
|
||||||
* @param source The Base64 encoded data
|
|
||||||
* @param off The offset of where to begin decoding
|
|
||||||
* @param len The length of characters to decode
|
|
||||||
* @param decodabet the decodabet for decoding Base64 content
|
|
||||||
* @return decoded data
|
|
||||||
*/
|
|
||||||
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
|
||||||
throws Base64DecoderException {
|
|
||||||
int len34 = len * 3 / 4;
|
|
||||||
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
|
||||||
int outBuffPosn = 0;
|
|
||||||
|
|
||||||
byte[] b4 = new byte[4];
|
|
||||||
int b4Posn = 0;
|
|
||||||
int i = 0;
|
|
||||||
byte sbiCrop = 0;
|
|
||||||
byte sbiDecode = 0;
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
|
|
||||||
sbiDecode = decodabet[sbiCrop];
|
|
||||||
|
|
||||||
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
|
||||||
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
|
||||||
// An equals sign (for padding) must not occur at position 0 or 1
|
|
||||||
// and must be the last byte[s] in the encoded value
|
|
||||||
if (sbiCrop == EQUALS_SIGN) {
|
|
||||||
int bytesLeft = len - i;
|
|
||||||
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
|
|
||||||
if (b4Posn == 0 || b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"invalid padding byte '=' at byte offset " + i);
|
|
||||||
} else if ((b4Posn == 3 && bytesLeft > 2)
|
|
||||||
|| (b4Posn == 4 && bytesLeft > 1)) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"padding byte '=' falsely signals end of encoded value "
|
|
||||||
+ "at offset " + i);
|
|
||||||
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
|
||||||
throw new Base64DecoderException(
|
|
||||||
"encoded value has invalid trailing byte");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
b4[b4Posn++] = sbiCrop;
|
|
||||||
if (b4Posn == 4) {
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
b4Posn = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Base64DecoderException("Bad Base64 input character at " + i
|
|
||||||
+ ": " + source[i + off] + "(decimal)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because web safe encoding allows non padding base64 encodes, we
|
|
||||||
// need to pad the rest of the b4 buffer with equal signs when
|
|
||||||
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
|
||||||
// four characters, so the b4 buffer must have two or three
|
|
||||||
// characters. This also catches the case where the input is
|
|
||||||
// padded with EQUALS_SIGN
|
|
||||||
if (b4Posn != 0) {
|
|
||||||
if (b4Posn == 1) {
|
|
||||||
throw new Base64DecoderException("single trailing character at offset "
|
|
||||||
+ (len - 1));
|
|
||||||
}
|
|
||||||
b4[b4Posn++] = EQUALS_SIGN;
|
|
||||||
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] out = new byte[outBuffPosn];
|
|
||||||
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,119 +18,113 @@ package com.google.android.vending.expansion.downloader;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the internal constants that are used in the download manager.
|
* Contains the internal constants that are used in the download manager.
|
||||||
* As a general rule, modifying these constants should be done with care.
|
* As a general rule, modifying these constants should be done with care.
|
||||||
*/
|
*/
|
||||||
public class Constants {
|
public class Constants {
|
||||||
/** Tag used for debugging/logging */
|
/** Tag used for debugging/logging */
|
||||||
public static final String TAG = "LVLDL";
|
public static final String TAG = "LVLDL";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expansion path where we store obb files
|
* Expansion path where we store obb files
|
||||||
*/
|
*/
|
||||||
public static final String EXP_PATH = File.separator + "Android"
|
public static final String EXP_PATH = File.separator + "Android" + File.separator + "obb" + File.separator;
|
||||||
+ File.separator + "obb" + File.separator;
|
|
||||||
|
|
||||||
// save to private app's data on Android 6.0 to skip requesting permission.
|
/** The intent that gets sent when the service must wake up for a retry */
|
||||||
public static final String EXP_PATH_API23 = File.separator + "Android"
|
public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
|
||||||
+ File.separator + "data" + File.separator;
|
|
||||||
|
|
||||||
/** The intent that gets sent when the service must wake up for a retry */
|
/** the intent that gets sent when clicking a successful download */
|
||||||
public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
|
public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
|
||||||
|
|
||||||
/** the intent that gets sent when clicking a successful download */
|
/** the intent that gets sent when clicking an incomplete/failed download */
|
||||||
public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
|
public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
|
||||||
|
|
||||||
/** the intent that gets sent when clicking an incomplete/failed download */
|
/** the intent that gets sent when deleting the notification of a completed download */
|
||||||
public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
|
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
|
* When a number has to be appended to the filename, this string is used to separate the
|
||||||
* base filename from the sequence number
|
* 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 */
|
/** The default user agent used for downloads */
|
||||||
public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
|
public static final String DEFAULT_USER_AGENT = "Android.LVLDM";
|
||||||
|
|
||||||
/** The buffer size used to stream the data */
|
/** The buffer size used to stream the data */
|
||||||
public static final int BUFFER_SIZE = 4096;
|
public static final int BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
/** The minimum amount of progress that has to be done before the progress bar gets updated */
|
/** The minimum amount of progress that has to be done before the progress bar gets updated */
|
||||||
public static final int MIN_PROGRESS_STEP = 4096;
|
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 */
|
/** 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;
|
public static final long MIN_PROGRESS_TIME = 1000;
|
||||||
|
|
||||||
/** The maximum number of rows in the database (FIFO) */
|
/** The maximum number of rows in the database (FIFO) */
|
||||||
public static final int MAX_DOWNLOADS = 1000;
|
public static final int MAX_DOWNLOADS = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of times that the download manager will retry its network
|
* The number of times that the download manager will retry its network
|
||||||
* operations when no progress is happening before it gives up.
|
* 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
|
* The minimum amount of time that the download manager accepts for
|
||||||
* a Retry-After response header with a parameter in delta-seconds.
|
* 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
|
* The maximum amount of time that the download manager accepts for
|
||||||
* a Retry-After response header with a parameter in delta-seconds.
|
* 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.
|
* 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.
|
* The time between a failure and the first retry after an IOException.
|
||||||
* Each subsequent retry grows exponentially, doubling each time.
|
* Each subsequent retry grows exponentially, doubling each time.
|
||||||
* The time is in seconds.
|
* 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 */
|
/** Enable separate connectivity logging */
|
||||||
public static final boolean LOGX = true;
|
public static final boolean LOGX = true;
|
||||||
|
|
||||||
/** Enable verbose logging */
|
/** Enable verbose logging */
|
||||||
public static final boolean LOGV = false;
|
public static final boolean LOGV = false;
|
||||||
|
|
||||||
/** Enable super-verbose logging */
|
/** Enable super-verbose logging */
|
||||||
private static final boolean LOCAL_LOGVV = false;
|
private static final boolean LOCAL_LOGVV = false;
|
||||||
public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
|
public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This download has successfully completed.
|
* This download has successfully completed.
|
||||||
* Warning: there might be other status values that indicate success
|
* Warning: there might be other status values that indicate success
|
||||||
* in the future.
|
* in the future.
|
||||||
* Use isSucccess() to capture the entire category.
|
* 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
|
* This request couldn't be parsed. This is also used when processing
|
||||||
* requests with unknown/unsupported URI schemes.
|
* 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
|
* This download can't be performed because the content type cannot be
|
||||||
* handled.
|
* 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
|
* This download cannot be performed because the length cannot be
|
||||||
* determined accurately. This is the code for the HTTP error "Length
|
* determined accurately. This is the code for the HTTP error "Length
|
||||||
* Required", which is typically used when making requests that require
|
* 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
|
* accurately (therefore making it impossible to know when a download
|
||||||
* completes).
|
* 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 download was interrupted and cannot be resumed.
|
||||||
* This is the code for the HTTP error "Precondition Failed", and it is
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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
|
* 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.
|
* This download has completed with an error.
|
||||||
* Warning: there will be other status values that indicate errors in
|
* Warning: there will be other status values that indicate errors in
|
||||||
* the future. Use isStatusError() to capture the entire category.
|
* 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.
|
* This download couldn't be completed because of a storage issue.
|
||||||
* Typically, that's because the filesystem is missing or full.
|
* Typically, that's because the filesystem is missing or full.
|
||||||
* Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
|
* Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
|
||||||
* and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
|
* 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
|
* This download couldn't be completed because of an HTTP
|
||||||
* redirect response that the download manager couldn't
|
* redirect response that the download manager couldn't
|
||||||
* handle.
|
* 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
|
* This download couldn't be completed because of an
|
||||||
* unspecified unhandled HTTP code.
|
* 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
|
* This download couldn't be completed because of an
|
||||||
* error receiving or processing data at the HTTP level.
|
* 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
|
* This download couldn't be completed because of an
|
||||||
* HttpException while setting up the request.
|
* 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
|
* This download couldn't be completed because there were
|
||||||
* too many redirects.
|
* 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
|
* This download couldn't be completed due to insufficient storage
|
||||||
* space. Typically, this is because the SD card is full.
|
* 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
|
* This download couldn't be completed because no external storage
|
||||||
* device was found. Typically, this is because the SD card is not
|
* device was found. Typically, this is because the SD card is not
|
||||||
* mounted.
|
* 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.
|
* 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.
|
* The wake duration to check to see if the process was killed.
|
||||||
*/
|
*/
|
||||||
public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;
|
public static final long ACTIVE_THREAD_WATCHDOG = 5 * 1000;
|
||||||
|
|
||||||
}
|
}
|
@ -19,7 +19,6 @@ package com.google.android.vending.expansion.downloader;
|
|||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class contains progress information about the active download(s).
|
* 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.
|
* as the progress so far, time remaining and current speed.
|
||||||
*/
|
*/
|
||||||
public class DownloadProgressInfo implements Parcelable {
|
public class DownloadProgressInfo implements Parcelable {
|
||||||
public long mOverallTotal;
|
public long mOverallTotal;
|
||||||
public long mOverallProgress;
|
public long mOverallProgress;
|
||||||
public long mTimeRemaining; // time remaining
|
public long mTimeRemaining; // time remaining
|
||||||
public float mCurrentSpeed; // speed in KB/S
|
public float mCurrentSpeed; // speed in KB/S
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public int describeContents() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel p, int i) {
|
public void writeToParcel(Parcel p, int i) {
|
||||||
p.writeLong(mOverallTotal);
|
p.writeLong(mOverallTotal);
|
||||||
p.writeLong(mOverallProgress);
|
p.writeLong(mOverallProgress);
|
||||||
p.writeLong(mTimeRemaining);
|
p.writeLong(mTimeRemaining);
|
||||||
p.writeFloat(mCurrentSpeed);
|
p.writeFloat(mCurrentSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadProgressInfo(Parcel p) {
|
public DownloadProgressInfo(Parcel p) {
|
||||||
mOverallTotal = p.readLong();
|
mOverallTotal = p.readLong();
|
||||||
mOverallProgress = p.readLong();
|
mOverallProgress = p.readLong();
|
||||||
mTimeRemaining = p.readLong();
|
mTimeRemaining = p.readLong();
|
||||||
mCurrentSpeed = p.readFloat();
|
mCurrentSpeed = p.readFloat();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadProgressInfo(long overallTotal, long overallProgress,
|
public DownloadProgressInfo(long overallTotal, long overallProgress,
|
||||||
long timeRemaining,
|
long timeRemaining,
|
||||||
float currentSpeed) {
|
float currentSpeed) {
|
||||||
this.mOverallTotal = overallTotal;
|
this.mOverallTotal = overallTotal;
|
||||||
this.mOverallProgress = overallProgress;
|
this.mOverallProgress = overallProgress;
|
||||||
this.mTimeRemaining = timeRemaining;
|
this.mTimeRemaining = timeRemaining;
|
||||||
this.mCurrentSpeed = currentSpeed;
|
this.mCurrentSpeed = currentSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
|
public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public DownloadProgressInfo createFromParcel(Parcel parcel) {
|
public DownloadProgressInfo createFromParcel(Parcel parcel) {
|
||||||
return new DownloadProgressInfo(parcel);
|
return new DownloadProgressInfo(parcel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public DownloadProgressInfo[] newArray(int i) {
|
|
||||||
return new DownloadProgressInfo[i];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadProgressInfo[] newArray(int i) {
|
||||||
|
return new DownloadProgressInfo[i];
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ import android.os.Messenger;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
|
* This class binds the service API to your application client. It contains the IDownloaderClient proxy,
|
||||||
@ -58,158 +58,172 @@ import android.util.Log;
|
|||||||
* interface.
|
* interface.
|
||||||
*/
|
*/
|
||||||
public class DownloaderClientMarshaller {
|
public class DownloaderClientMarshaller {
|
||||||
public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
|
public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10;
|
||||||
public static final int MSG_ONDOWNLOADPROGRESS = 11;
|
public static final int MSG_ONDOWNLOADPROGRESS = 11;
|
||||||
public static final int MSG_ONSERVICECONNECTED = 12;
|
public static final int MSG_ONSERVICECONNECTED = 12;
|
||||||
|
|
||||||
public static final String PARAM_NEW_STATE = "newState";
|
public static final String PARAM_NEW_STATE = "newState";
|
||||||
public static final String PARAM_PROGRESS = "progress";
|
public static final String PARAM_PROGRESS = "progress";
|
||||||
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
|
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 NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED;
|
||||||
public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_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 DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED;
|
||||||
|
|
||||||
private static class Proxy implements IDownloaderClient {
|
private static class Proxy implements IDownloaderClient {
|
||||||
private Messenger mServiceMessenger;
|
private Messenger mServiceMessenger;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownloadStateChanged(int newState) {
|
public void onDownloadStateChanged(int newState) {
|
||||||
Bundle params = new Bundle(1);
|
Bundle params = new Bundle(1);
|
||||||
params.putInt(PARAM_NEW_STATE, newState);
|
params.putInt(PARAM_NEW_STATE, newState);
|
||||||
send(MSG_ONDOWNLOADSTATE_CHANGED, params);
|
send(MSG_ONDOWNLOADSTATE_CHANGED, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownloadProgress(DownloadProgressInfo progress) {
|
public void onDownloadProgress(DownloadProgressInfo progress) {
|
||||||
Bundle params = new Bundle(1);
|
Bundle params = new Bundle(1);
|
||||||
params.putParcelable(PARAM_PROGRESS, progress);
|
params.putParcelable(PARAM_PROGRESS, progress);
|
||||||
send(MSG_ONDOWNLOADPROGRESS, params);
|
send(MSG_ONDOWNLOADPROGRESS, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send(int method, Bundle params) {
|
private void send(int method, Bundle params) {
|
||||||
Message m = Message.obtain(null, method);
|
Message m = Message.obtain(null, method);
|
||||||
m.setData(params);
|
m.setData(params);
|
||||||
try {
|
try {
|
||||||
mServiceMessenger.send(m);
|
mServiceMessenger.send(m);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Proxy(Messenger msg) {
|
public Proxy(Messenger msg) {
|
||||||
mServiceMessenger = msg;
|
mServiceMessenger = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(Messenger m) {
|
public void onServiceConnected(Messenger m) {
|
||||||
/**
|
/**
|
||||||
* This is never called through the proxy.
|
* This is never called through the proxy.
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Stub implements IStub {
|
private static class Stub implements IStub {
|
||||||
private IDownloaderClient mItf = null;
|
private IDownloaderClient mItf = null;
|
||||||
private Class<?> mDownloaderServiceClass;
|
private Class<?> mDownloaderServiceClass;
|
||||||
private boolean mBound;
|
private boolean mBound;
|
||||||
private Messenger mServiceMessenger;
|
private Messenger mServiceMessenger;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
/**
|
/**
|
||||||
* Target we publish for clients to send messages to IncomingHandler.
|
* Target we publish for clients to send messages to IncomingHandler.
|
||||||
*/
|
*/
|
||||||
final Messenger mMessenger = new Messenger(new Handler() {
|
private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
|
||||||
@Override
|
final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
|
private static class MessengerHandlerClient extends Handler {
|
||||||
mItf = itf;
|
private final WeakReference<Stub> mDownloader;
|
||||||
mDownloaderServiceClass = downloaderService;
|
public MessengerHandlerClient(Stub downloader) {
|
||||||
}
|
mDownloader = new WeakReference<>(downloader);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
Stub downloader = mDownloader.get();
|
||||||
|
if (downloader != null) {
|
||||||
|
downloader.handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_ONDOWNLOADPROGRESS:
|
||||||
|
Bundle bun = msg.getData();
|
||||||
|
if (null != mContext) {
|
||||||
|
bun.setClassLoader(mContext.getClassLoader());
|
||||||
|
DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData()
|
||||||
|
.getParcelable(PARAM_PROGRESS);
|
||||||
|
mItf.onDownloadProgress(dpi);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MSG_ONDOWNLOADSTATE_CHANGED:
|
||||||
|
mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE));
|
||||||
|
break;
|
||||||
|
case MSG_ONSERVICECONNECTED:
|
||||||
|
mItf.onServiceConnected(
|
||||||
|
(Messenger)msg.getData().getParcelable(PARAM_MESSENGER));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stub(IDownloaderClient itf, Class<?> downloaderService) {
|
||||||
|
mItf = itf;
|
||||||
|
mDownloaderServiceClass = downloaderService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Class for interacting with the main interface of the service.
|
* Class for interacting with the main interface of the service.
|
||||||
*/
|
*/
|
||||||
private ServiceConnection mConnection = new ServiceConnection() {
|
private ServiceConnection mConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||||
// This is called when the connection with the service has been
|
// This is called when the connection with the service has been
|
||||||
// established, giving us the object we can use to
|
// established, giving us the object we can use to
|
||||||
// interact with the service. We are communicating with the
|
// interact with the service. We are communicating with the
|
||||||
// service using a Messenger, so here we get a client-side
|
// service using a Messenger, so here we get a client-side
|
||||||
// representation of that from the raw IBinder object.
|
// representation of that from the raw IBinder object.
|
||||||
mServiceMessenger = new Messenger(service);
|
mServiceMessenger = new Messenger(service);
|
||||||
mItf.onServiceConnected(
|
mItf.onServiceConnected(
|
||||||
mServiceMessenger);
|
mServiceMessenger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
// This is called when the connection with the service has been
|
// This is called when the connection with the service has been
|
||||||
// unexpectedly disconnected -- that is, its process crashed.
|
// unexpectedly disconnected -- that is, its process crashed.
|
||||||
mServiceMessenger = null;
|
mServiceMessenger = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connect(Context c) {
|
public void connect(Context c) {
|
||||||
mContext = c;
|
mContext = c;
|
||||||
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
|
Intent bindIntent = new Intent(c, mDownloaderServiceClass);
|
||||||
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
|
bindIntent.putExtra(PARAM_MESSENGER, mMessenger);
|
||||||
if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) {
|
if (!c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND)) {
|
||||||
if ( Constants.LOGVV ) {
|
if (Constants.LOGVV) {
|
||||||
Log.d(Constants.TAG, "Service Unbound");
|
Log.d(Constants.TAG, "Service Unbound");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mBound = true;
|
mBound = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
@Override
|
||||||
|
public void disconnect(Context c) {
|
||||||
|
if (mBound) {
|
||||||
|
c.unbindService(mConnection);
|
||||||
|
mBound = false;
|
||||||
|
}
|
||||||
|
mContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnect(Context c) {
|
public Messenger getMessenger() {
|
||||||
if (mBound) {
|
return mMessenger;
|
||||||
c.unbindService(mConnection);
|
}
|
||||||
mBound = false;
|
}
|
||||||
}
|
|
||||||
mContext = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public Messenger getMessenger() {
|
|
||||||
return mMessenger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a proxy that will marshal calls to IDownloaderClient methods
|
* Returns a proxy that will marshal calls to IDownloaderClient methods
|
||||||
*
|
*
|
||||||
* @param msg
|
* @param msg
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static IDownloaderClient CreateProxy(Messenger msg) {
|
public static IDownloaderClient CreateProxy(Messenger msg) {
|
||||||
return new Proxy(msg);
|
return new Proxy(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a stub object that, when connected, will listen for marshaled
|
* Returns a stub object that, when connected, will listen for marshaled
|
||||||
* {@link IDownloaderClient} methods and translate them into calls to the supplied
|
* {@link IDownloaderClient} methods and translate them into calls to the supplied
|
||||||
* interface.
|
* interface.
|
||||||
@ -221,11 +235,11 @@ public class DownloaderClientMarshaller {
|
|||||||
* @return The {@link IStub} that allows you to connect to the service such that
|
* @return The {@link IStub} that allows you to connect to the service such that
|
||||||
* your {@link IDownloaderClient} receives status updates.
|
* your {@link IDownloaderClient} receives status updates.
|
||||||
*/
|
*/
|
||||||
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
|
public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) {
|
||||||
return new Stub(itf, downloaderService);
|
return new Stub(itf, downloaderService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the download if necessary. This function starts a flow that does `
|
* 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
|
* 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,
|
* the metadata database updated 2) If the APK version does not match,
|
||||||
@ -248,14 +262,14 @@ public class DownloaderClientMarshaller {
|
|||||||
* #DOWNLOAD_REQUIRED}.
|
* #DOWNLOAD_REQUIRED}.
|
||||||
* @throws NameNotFoundException
|
* @throws NameNotFoundException
|
||||||
*/
|
*/
|
||||||
public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
|
public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,
|
||||||
Class<?> serviceClass)
|
Class<?> serviceClass)
|
||||||
throws NameNotFoundException {
|
throws NameNotFoundException {
|
||||||
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
||||||
serviceClass);
|
serviceClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This version assumes that the intent contains the pending intent as a parameter. This
|
* This version assumes that the intent contains the pending intent as a parameter. This
|
||||||
* is used for responding to alarms.
|
* 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
|
||||||
@ -267,11 +281,10 @@ public class DownloaderClientMarshaller {
|
|||||||
* @return
|
* @return
|
||||||
* @throws NameNotFoundException
|
* @throws NameNotFoundException
|
||||||
*/
|
*/
|
||||||
public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
|
public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,
|
||||||
Class<?> serviceClass)
|
Class<?> serviceClass)
|
||||||
throws NameNotFoundException {
|
throws NameNotFoundException {
|
||||||
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
return DownloaderService.startDownloadServiceIfRequired(context, notificationClient,
|
||||||
serviceClass);
|
serviceClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import android.os.Message;
|
|||||||
import android.os.Messenger;
|
import android.os.Messenger;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used by the client activity to proxy requests to the Downloader
|
* This class is used by the client activity to proxy requests to the Downloader
|
||||||
@ -38,134 +38,147 @@ import android.os.RemoteException;
|
|||||||
*/
|
*/
|
||||||
public class DownloaderServiceMarshaller {
|
public class DownloaderServiceMarshaller {
|
||||||
|
|
||||||
public static final int MSG_REQUEST_ABORT_DOWNLOAD =
|
public static final int MSG_REQUEST_ABORT_DOWNLOAD =
|
||||||
1;
|
1;
|
||||||
public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
|
public static final int MSG_REQUEST_PAUSE_DOWNLOAD =
|
||||||
2;
|
2;
|
||||||
public static final int MSG_SET_DOWNLOAD_FLAGS =
|
public static final int MSG_SET_DOWNLOAD_FLAGS =
|
||||||
3;
|
3;
|
||||||
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
|
public static final int MSG_REQUEST_CONTINUE_DOWNLOAD =
|
||||||
4;
|
4;
|
||||||
public static final int MSG_REQUEST_DOWNLOAD_STATE =
|
public static final int MSG_REQUEST_DOWNLOAD_STATE =
|
||||||
5;
|
5;
|
||||||
public static final int MSG_REQUEST_CLIENT_UPDATE =
|
public static final int MSG_REQUEST_CLIENT_UPDATE =
|
||||||
6;
|
6;
|
||||||
|
|
||||||
public static final String PARAMS_FLAGS = "flags";
|
public static final String PARAMS_FLAGS = "flags";
|
||||||
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
|
public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER;
|
||||||
|
|
||||||
private static class Proxy implements IDownloaderService {
|
private static class Proxy implements IDownloaderService {
|
||||||
private Messenger mMsg;
|
private Messenger mMsg;
|
||||||
|
|
||||||
private void send(int method, Bundle params) {
|
private void send(int method, Bundle params) {
|
||||||
Message m = Message.obtain(null, method);
|
Message m = Message.obtain(null, method);
|
||||||
m.setData(params);
|
m.setData(params);
|
||||||
try {
|
try {
|
||||||
mMsg.send(m);
|
mMsg.send(m);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Proxy(Messenger msg) {
|
public Proxy(Messenger msg) {
|
||||||
mMsg = msg;
|
mMsg = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestAbortDownload() {
|
public void requestAbortDownload() {
|
||||||
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
|
send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestPauseDownload() {
|
public void requestPauseDownload() {
|
||||||
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
|
send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDownloadFlags(int flags) {
|
public void setDownloadFlags(int flags) {
|
||||||
Bundle params = new Bundle();
|
Bundle params = new Bundle();
|
||||||
params.putInt(PARAMS_FLAGS, flags);
|
params.putInt(PARAMS_FLAGS, flags);
|
||||||
send(MSG_SET_DOWNLOAD_FLAGS, params);
|
send(MSG_SET_DOWNLOAD_FLAGS, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestContinueDownload() {
|
public void requestContinueDownload() {
|
||||||
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
|
send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requestDownloadStatus() {
|
public void requestDownloadStatus() {
|
||||||
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
|
send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClientUpdated(Messenger clientMessenger) {
|
public void onClientUpdated(Messenger clientMessenger) {
|
||||||
Bundle bundle = new Bundle(1);
|
Bundle bundle = new Bundle(1);
|
||||||
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
|
bundle.putParcelable(PARAM_MESSENGER, clientMessenger);
|
||||||
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
|
send(MSG_REQUEST_CLIENT_UPDATE, bundle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Stub implements IStub {
|
private static class Stub implements IStub {
|
||||||
private IDownloaderService mItf = null;
|
private IDownloaderService mItf = null;
|
||||||
final Messenger mMessenger = new Messenger(new Handler() {
|
private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
|
||||||
@Override
|
final Messenger mMessenger = new Messenger(mMsgHandler);
|
||||||
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) {
|
private static class MessengerHandlerServer extends Handler {
|
||||||
mItf = itf;
|
private final WeakReference<Stub> mDownloader;
|
||||||
}
|
public MessengerHandlerServer(Stub downloader) {
|
||||||
|
mDownloader = new WeakReference<>(downloader);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Messenger getMessenger() {
|
public void handleMessage(Message msg) {
|
||||||
return mMessenger;
|
Stub downloader = mDownloader.get();
|
||||||
}
|
if (downloader != null) {
|
||||||
|
downloader.handleMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private void handleMessage(Message msg) {
|
||||||
public void connect(Context c) {
|
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
|
@Override
|
||||||
public void disconnect(Context c) {
|
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
|
* Returns a proxy that will marshall calls to IDownloaderService methods
|
||||||
*
|
*
|
||||||
* @param ctx
|
* @param ctx
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static IDownloaderService CreateProxy(Messenger msg) {
|
public static IDownloaderService CreateProxy(Messenger msg) {
|
||||||
return new Proxy(msg);
|
return new Proxy(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a stub object that, when connected, will listen for marshalled
|
* Returns a stub object that, when connected, will listen for marshalled
|
||||||
* IDownloaderService methods and translate them into calls to the supplied
|
* IDownloaderService methods and translate them into calls to the supplied
|
||||||
* interface.
|
* interface.
|
||||||
@ -174,8 +187,7 @@ public class DownloaderServiceMarshaller {
|
|||||||
* when remote method calls are unmarshalled.
|
* when remote method calls are unmarshalled.
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static IStub CreateStub(IDownloaderService itf) {
|
public static IStub CreateStub(IDownloaderService itf) {
|
||||||
return new Stub(itf);
|
return new Stub(itf);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,7 @@
|
|||||||
|
|
||||||
package com.google.android.vending.expansion.downloader;
|
package com.google.android.vending.expansion.downloader;
|
||||||
|
|
||||||
import com.godot.game.R;
|
import android.annotation.TargetApi;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
@ -25,6 +24,8 @@ import android.os.StatFs;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.godot.game.R;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -39,273 +40,316 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public class Helpers {
|
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 */
|
/** Regex used to parse content-disposition headers */
|
||||||
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
|
private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
|
||||||
.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
|
.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
|
||||||
|
|
||||||
private Helpers() {
|
private Helpers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parse the Content-Disposition HTTP Header. The format of the header is
|
* Parse the Content-Disposition HTTP Header. The format of the header is defined here:
|
||||||
* defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
|
||||||
* header provides a filename for content that is going to be downloaded to
|
* content that is going to be downloaded to the file system. We only support the attachment
|
||||||
* the file system. We only support the attachment type.
|
* type.
|
||||||
*/
|
*/
|
||||||
static String parseContentDisposition(String contentDisposition) {
|
static String parseContentDisposition(String contentDisposition) {
|
||||||
try {
|
try {
|
||||||
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
|
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
|
||||||
if (m.find()) {
|
if (m.find()) {
|
||||||
return m.group(1);
|
return m.group(1);
|
||||||
}
|
}
|
||||||
} catch (IllegalStateException ex) {
|
} catch (IllegalStateException ex) {
|
||||||
// This function is defined as returning null when it can't parse
|
// This function is defined as returning null when it can't parse
|
||||||
// the header
|
// the header
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the root of the filesystem containing the given path
|
* @return the root of the filesystem containing the given path
|
||||||
*/
|
*/
|
||||||
public static File getFilesystemRoot(String path) {
|
public static File getFilesystemRoot(String path) {
|
||||||
File cache = Environment.getDownloadCacheDirectory();
|
File cache = Environment.getDownloadCacheDirectory();
|
||||||
if (path.startsWith(cache.getPath())) {
|
if (path.startsWith(cache.getPath())) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
File external = Environment.getExternalStorageDirectory();
|
File external = Environment.getExternalStorageDirectory();
|
||||||
if (path.startsWith(external.getPath())) {
|
if (path.startsWith(external.getPath())) {
|
||||||
return external;
|
return external;
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Cannot determine filesystem root for " + path);
|
"Cannot determine filesystem root for " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isExternalMediaMounted() {
|
public static boolean isExternalMediaMounted() {
|
||||||
if (!Environment.getExternalStorageState().equals(
|
if (!Environment.getExternalStorageState().equals(
|
||||||
Environment.MEDIA_MOUNTED)) {
|
Environment.MEDIA_MOUNTED)) {
|
||||||
// No SD card found.
|
// No SD card found.
|
||||||
if ( Constants.LOGVV ) {
|
if (Constants.LOGVV) {
|
||||||
Log.d(Constants.TAG, "no external storage");
|
Log.d(Constants.TAG, "no external storage");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the number of bytes available on the filesystem rooted at the
|
* @return the number of bytes available on the filesystem rooted at the given File
|
||||||
* given File
|
|
||||||
*/
|
*/
|
||||||
public static long getAvailableBytes(File root) {
|
public static long getAvailableBytes(File root) {
|
||||||
StatFs stat = new StatFs(root.getPath());
|
StatFs stat = new StatFs(root.getPath());
|
||||||
// put a bit of margin (in case creating the file grows the system by a
|
// put a bit of margin (in case creating the file grows the system by a
|
||||||
// few blocks)
|
// few blocks)
|
||||||
long availableBlocks = (long) stat.getAvailableBlocks() - 4;
|
long availableBlocks = (long)stat.getAvailableBlocks() - 4;
|
||||||
return stat.getBlockSize() * availableBlocks;
|
return stat.getBlockSize() * availableBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the filename looks legitimate
|
* Checks whether the filename looks legitimate
|
||||||
*/
|
*/
|
||||||
public static boolean isFilenameValid(String filename) {
|
public static boolean isFilenameValid(String filename) {
|
||||||
filename = filename.replaceFirst("/+", "/"); // normalize leading
|
filename = filename.replaceFirst("/+", "/"); // normalize leading
|
||||||
// slashes
|
// slashes
|
||||||
return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
|
return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) || filename.startsWith(Environment.getExternalStorageDirectory().toString());
|
||||||
|| filename.startsWith(Environment.getExternalStorageDirectory().toString());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Delete the given file from device
|
* Delete the given file from device
|
||||||
*/
|
*/
|
||||||
/* package */static void deleteFile(String path) {
|
/* package */ static void deleteFile(String path) {
|
||||||
try {
|
try {
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
file.delete();
|
file.delete();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", 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,
|
* Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total
|
||||||
* GB) based on total file size, but given what we know about the expected
|
* file size, but given what we know about the expected ranges of file sizes for APK expansion
|
||||||
* ranges of file sizes for APK expansion files, it's probably not necessary.
|
* files, it's probably not necessary.
|
||||||
*
|
*
|
||||||
* @param overallProgress
|
* @param overallProgress
|
||||||
* @param overallTotal
|
* @param overallTotal
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static public String getDownloadProgressString(long overallProgress, long overallTotal) {
|
static public String getDownloadProgressString(long overallProgress, long overallTotal) {
|
||||||
if (overallTotal == 0) {
|
if (overallTotal == 0) {
|
||||||
if ( Constants.LOGVV ) {
|
if (Constants.LOGVV) {
|
||||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return String.format("%.2f",
|
return String.format(Locale.ENGLISH, "%.2f",
|
||||||
(float) overallProgress / (1024.0f * 1024.0f))
|
(float)overallProgress / (1024.0f * 1024.0f)) +
|
||||||
+ "MB /" +
|
"MB /" +
|
||||||
String.format("%.2f", (float) overallTotal /
|
String.format(Locale.ENGLISH, "%.2f", (float)overallTotal / (1024.0f * 1024.0f)) + "MB";
|
||||||
(1024.0f * 1024.0f)) + "MB";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a percentile to getDownloadProgressString.
|
* Adds a percentile to getDownloadProgressString.
|
||||||
*
|
*
|
||||||
* @param overallProgress
|
* @param overallProgress
|
||||||
* @param overallTotal
|
* @param overallTotal
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
static public String getDownloadProgressStringNotification(long overallProgress,
|
static public String getDownloadProgressStringNotification(long overallProgress,
|
||||||
long overallTotal) {
|
long overallTotal) {
|
||||||
if (overallTotal == 0) {
|
if (overallTotal == 0) {
|
||||||
if ( Constants.LOGVV ) {
|
if (Constants.LOGVV) {
|
||||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return getDownloadProgressString(overallProgress, overallTotal) + " (" +
|
return getDownloadProgressString(overallProgress, overallTotal) + " (" +
|
||||||
getDownloadProgressPercent(overallProgress, overallTotal) + ")";
|
getDownloadProgressPercent(overallProgress, overallTotal) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
|
public static String getDownloadProgressPercent(long overallProgress, long overallTotal) {
|
||||||
if (overallTotal == 0) {
|
if (overallTotal == 0) {
|
||||||
if ( Constants.LOGVV ) {
|
if (Constants.LOGVV) {
|
||||||
Log.e(Constants.TAG, "Notification called when total is zero");
|
Log.e(Constants.TAG, "Notification called when total is zero");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return Long.toString(overallProgress * 100 / overallTotal) + "%";
|
return Long.toString(overallProgress * 100 / overallTotal) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getSpeedString(float bytesPerMillisecond) {
|
public static String getSpeedString(float bytesPerMillisecond) {
|
||||||
return String.format("%.2f", bytesPerMillisecond * 1000 / 1024);
|
return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getTimeRemaining(long durationInMilliseconds) {
|
public static String getTimeRemaining(long durationInMilliseconds) {
|
||||||
SimpleDateFormat sdf;
|
SimpleDateFormat sdf;
|
||||||
if (durationInMilliseconds > 1000 * 60 * 60) {
|
if (durationInMilliseconds > 1000 * 60 * 60) {
|
||||||
sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
||||||
} else {
|
} else {
|
||||||
sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
|
sdf = new SimpleDateFormat("mm:ss", Locale.getDefault());
|
||||||
}
|
}
|
||||||
return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
|
return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the file name (without full path) for an Expansion APK file from
|
* Returns the file name (without full path) for an Expansion APK file from the given context.
|
||||||
* the given context.
|
|
||||||
*
|
*
|
||||||
* @param c the context
|
* @param c the context
|
||||||
* @param mainFile true for main file, false for patch file
|
* @param mainFile true for main file, false for patch file
|
||||||
* @param versionCode the version of the file
|
* @param versionCode the version of the file
|
||||||
* @return String the file name of the expansion file
|
* @return String the file name of the expansion file
|
||||||
*/
|
*/
|
||||||
public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
|
public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) {
|
||||||
return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
|
return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the filename (where the file should be saved) from info about a
|
* Returns the filename (where the file should be saved) from info about a download
|
||||||
* download
|
|
||||||
*/
|
*/
|
||||||
static public String generateSaveFileName(Context c, String fileName) {
|
static public String generateSaveFileName(Context c, String fileName) {
|
||||||
String path = getSaveFilePath(c)
|
String path = getSaveFilePath(c) + File.separator + fileName;
|
||||||
+ File.separator + fileName;
|
return path;
|
||||||
return path;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static public String getSaveFilePath(Context c) {
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
File root = Environment.getExternalStorageDirectory();
|
static public String getSaveFilePath(Context c) {
|
||||||
// this makes several issues with Android SDK >= 23 devices.
|
// This technically existed since Honeycomb, but it is critical
|
||||||
// https://github.com/danikula/Google-Play-Expansion-File/commit/93a03bd34acad67c6ea34cfb6c3f02c93bdcea85
|
// on KitKat and greater versions since it will create the
|
||||||
// https://issuetracker.google.com/issues/37075181
|
// directory if needed
|
||||||
//String path = Build.VERSION.SDK_INT >= 23 ? Constants.EXP_PATH_API23 : Constants.EXP_PATH;
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
String path = Constants.EXP_PATH;
|
return c.getObbDir().toString();
|
||||||
return root.toString() + path + c.getPackageName();
|
} 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
|
* Helper function to ascertain the existence of a file and return true/false appropriately
|
||||||
* true/false appropriately
|
|
||||||
*
|
*
|
||||||
* @param c the app/activity/service context
|
* @param c the app/activity/service context
|
||||||
* @param fileName the name (sans path) of the file to query
|
* @param fileName the name (sans path) of the file to query
|
||||||
* @param fileSize the size that the file must match
|
* @param fileSize the size that the file must match
|
||||||
* @param deleteFileOnMismatch if the file sizes do not match, delete the
|
* @param deleteFileOnMismatch if the file sizes do not match, delete the file
|
||||||
* file
|
|
||||||
* @return true if it does exist, false otherwise
|
* @return true if it does exist, false otherwise
|
||||||
*/
|
*/
|
||||||
static public boolean doesFileExist(Context c, String fileName, long fileSize,
|
static public boolean doesFileExist(Context c, String fileName, long fileSize,
|
||||||
boolean deleteFileOnMismatch) {
|
boolean deleteFileOnMismatch) {
|
||||||
// the file may have been delivered by Market --- let's make sure
|
// the file may have been delivered by Play --- let's make sure
|
||||||
// it's the size we expect
|
// it's the size we expect
|
||||||
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
|
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
|
||||||
if (fileForNewFile.exists()) {
|
if (fileForNewFile.exists()) {
|
||||||
if (fileForNewFile.length() == fileSize) {
|
if (fileForNewFile.length() == fileSize) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (deleteFileOnMismatch) {
|
if (deleteFileOnMismatch) {
|
||||||
// delete the file --- we won't be able to resume
|
// delete the file --- we won't be able to resume
|
||||||
// because we cannot confirm the integrity of the file
|
// because we cannot confirm the integrity of the file
|
||||||
fileForNewFile.delete();
|
fileForNewFile.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static final int FS_READABLE = 0;
|
||||||
* Converts download states that are returned by the {@link
|
public static final int FS_DOES_NOT_EXIST = 1;
|
||||||
* IDownloaderClient#onDownloadStateChanged} callback into usable strings.
|
public static final int FS_CANNOT_READ = 2;
|
||||||
* This is useful if using the state strings built into the library to display user messages.
|
|
||||||
|
/**
|
||||||
|
* Helper function to ascertain whether a file can be read.
|
||||||
|
*
|
||||||
|
* @param c the app/activity/service context
|
||||||
|
* @param fileName the name (sans path) of the file to query
|
||||||
|
* @return true if it does exist, false otherwise
|
||||||
|
*/
|
||||||
|
static public int getFileStatus(Context c, String fileName) {
|
||||||
|
// the file may have been delivered by Play --- let's make sure
|
||||||
|
// it's the size we expect
|
||||||
|
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
|
||||||
|
int returnValue;
|
||||||
|
if (fileForNewFile.exists()) {
|
||||||
|
if (fileForNewFile.canRead()) {
|
||||||
|
returnValue = FS_READABLE;
|
||||||
|
} else {
|
||||||
|
returnValue = FS_CANNOT_READ;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
returnValue = FS_DOES_NOT_EXIST;
|
||||||
|
}
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to ascertain whether the application has the correct access to the OBB
|
||||||
|
* directory to allow an OBB file to be written.
|
||||||
|
*
|
||||||
|
* @param c the app/activity/service context
|
||||||
|
* @return true if the application can write an OBB file, false otherwise
|
||||||
|
*/
|
||||||
|
static public boolean canWriteOBBFile(Context c) {
|
||||||
|
String path = getSaveFilePath(c);
|
||||||
|
File fileForNewFile = new File(path);
|
||||||
|
boolean canWrite;
|
||||||
|
if (fileForNewFile.exists()) {
|
||||||
|
canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
|
||||||
|
} else {
|
||||||
|
canWrite = fileForNewFile.mkdirs();
|
||||||
|
}
|
||||||
|
return canWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts download states that are returned by the
|
||||||
|
* {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful
|
||||||
|
* if using the state strings built into the library to display user messages.
|
||||||
|
*
|
||||||
* @param state One of the STATE_* constants from {@link IDownloaderClient}.
|
* @param state One of the STATE_* constants from {@link IDownloaderClient}.
|
||||||
* @return string resource ID for the corresponding string.
|
* @return string resource ID for the corresponding string.
|
||||||
*/
|
*/
|
||||||
static public int getDownloaderStringResourceIDFromState(int state) {
|
static public int getDownloaderStringResourceIDFromState(int state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case IDownloaderClient.STATE_IDLE:
|
case IDownloaderClient.STATE_IDLE:
|
||||||
return R.string.state_idle;
|
return R.string.state_idle;
|
||||||
case IDownloaderClient.STATE_FETCHING_URL:
|
case IDownloaderClient.STATE_FETCHING_URL:
|
||||||
return R.string.state_fetching_url;
|
return R.string.state_fetching_url;
|
||||||
case IDownloaderClient.STATE_CONNECTING:
|
case IDownloaderClient.STATE_CONNECTING:
|
||||||
return R.string.state_connecting;
|
return R.string.state_connecting;
|
||||||
case IDownloaderClient.STATE_DOWNLOADING:
|
case IDownloaderClient.STATE_DOWNLOADING:
|
||||||
return R.string.state_downloading;
|
return R.string.state_downloading;
|
||||||
case IDownloaderClient.STATE_COMPLETED:
|
case IDownloaderClient.STATE_COMPLETED:
|
||||||
return R.string.state_completed;
|
return R.string.state_completed;
|
||||||
case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
|
case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE:
|
||||||
return R.string.state_paused_network_unavailable;
|
return R.string.state_paused_network_unavailable;
|
||||||
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
||||||
return R.string.state_paused_by_request;
|
return R.string.state_paused_by_request;
|
||||||
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
|
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
|
||||||
return R.string.state_paused_wifi_disabled;
|
return R.string.state_paused_wifi_disabled;
|
||||||
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
|
case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
|
||||||
return R.string.state_paused_wifi_unavailable;
|
return R.string.state_paused_wifi_unavailable;
|
||||||
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
|
case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED:
|
||||||
return R.string.state_paused_wifi_disabled;
|
return R.string.state_paused_wifi_disabled;
|
||||||
case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
|
case IDownloaderClient.STATE_PAUSED_NEED_WIFI:
|
||||||
return R.string.state_paused_wifi_unavailable;
|
return R.string.state_paused_wifi_unavailable;
|
||||||
case IDownloaderClient.STATE_PAUSED_ROAMING:
|
case IDownloaderClient.STATE_PAUSED_ROAMING:
|
||||||
return R.string.state_paused_roaming;
|
return R.string.state_paused_roaming;
|
||||||
case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
|
case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE:
|
||||||
return R.string.state_paused_network_setup_failure;
|
return R.string.state_paused_network_setup_failure;
|
||||||
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
|
case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
|
||||||
return R.string.state_paused_sdcard_unavailable;
|
return R.string.state_paused_sdcard_unavailable;
|
||||||
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
||||||
return R.string.state_failed_unlicensed;
|
return R.string.state_failed_unlicensed;
|
||||||
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
||||||
return R.string.state_failed_fetching_url;
|
return R.string.state_failed_fetching_url;
|
||||||
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
||||||
return R.string.state_failed_sdcard_full;
|
return R.string.state_failed_sdcard_full;
|
||||||
case IDownloaderClient.STATE_FAILED_CANCELED:
|
case IDownloaderClient.STATE_FAILED_CANCELED:
|
||||||
return R.string.state_failed_cancelled;
|
return R.string.state_failed_cancelled;
|
||||||
default:
|
default:
|
||||||
return R.string.state_unknown;
|
return R.string.state_unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,26 +23,26 @@ import android.os.Messenger;
|
|||||||
* downloader. It is used to pass status from the service to the client.
|
* downloader. It is used to pass status from the service to the client.
|
||||||
*/
|
*/
|
||||||
public interface IDownloaderClient {
|
public interface IDownloaderClient {
|
||||||
static final int STATE_IDLE = 1;
|
static final int STATE_IDLE = 1;
|
||||||
static final int STATE_FETCHING_URL = 2;
|
static final int STATE_FETCHING_URL = 2;
|
||||||
static final int STATE_CONNECTING = 3;
|
static final int STATE_CONNECTING = 3;
|
||||||
static final int STATE_DOWNLOADING = 4;
|
static final int STATE_DOWNLOADING = 4;
|
||||||
static final int STATE_COMPLETED = 5;
|
static final int STATE_COMPLETED = 5;
|
||||||
|
|
||||||
static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
|
static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6;
|
||||||
static final int STATE_PAUSED_BY_REQUEST = 7;
|
static final int STATE_PAUSED_BY_REQUEST = 7;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
|
* Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and
|
||||||
* STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
|
* STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and
|
||||||
* cellular permission will restart the service. Wi-Fi disabled means that
|
* 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
|
* the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the
|
||||||
* other case Wi-Fi is enabled but not available.
|
* 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_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8;
|
||||||
static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
|
static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
|
* Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that
|
||||||
* Wi-Fi is unavailable and cellular permission will NOT restart the
|
* Wi-Fi is unavailable and cellular permission will NOT restart the
|
||||||
* service. Wi-Fi disabled means that the Wi-Fi manager is returning that
|
* 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
|
* developers with very large payloads do not allow these payloads to be
|
||||||
* downloaded over cellular connections.
|
* downloaded over cellular connections.
|
||||||
*/
|
*/
|
||||||
static final int STATE_PAUSED_WIFI_DISABLED = 10;
|
static final int STATE_PAUSED_WIFI_DISABLED = 10;
|
||||||
static final int STATE_PAUSED_NEED_WIFI = 11;
|
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
|
* Scary case. We were on a network that redirected us to another website
|
||||||
* that delivered us the wrong file.
|
* 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_UNLICENSED = 15;
|
||||||
static final int STATE_FAILED_FETCHING_URL = 16;
|
static final int STATE_FAILED_FETCHING_URL = 16;
|
||||||
static final int STATE_FAILED_SDCARD_FULL = 17;
|
static final int STATE_FAILED_SDCARD_FULL = 17;
|
||||||
static final int STATE_FAILED_CANCELED = 18;
|
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.
|
* Called internally by the stub when the service is bound to the client.
|
||||||
* <p>
|
* <p>
|
||||||
* Critical implementation detail. In onServiceConnected we create the
|
* Critical implementation detail. In onServiceConnected we create the
|
||||||
@ -90,9 +90,9 @@ public interface IDownloaderClient {
|
|||||||
* @param m the service Messenger. This Messenger is used to call the
|
* @param m the service Messenger. This Messenger is used to call the
|
||||||
* service API from the client.
|
* 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
|
* 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
|
* 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.
|
* middle of a user request, so the client should be able to handle this.
|
||||||
@ -112,9 +112,9 @@ public interface IDownloaderClient {
|
|||||||
*
|
*
|
||||||
* @param newState one of the STATE_* values defined in IDownloaderClient
|
* @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
|
* 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
|
* client UI. This progress should only be shown in a few states such as
|
||||||
* STATE_DOWNLOADING.
|
* STATE_DOWNLOADING.
|
||||||
@ -122,5 +122,5 @@ public interface IDownloaderClient {
|
|||||||
* @param progress the DownloadProgressInfo object containing the current
|
* @param progress the DownloadProgressInfo object containing the current
|
||||||
* progress of all downloads.
|
* progress of all downloads.
|
||||||
*/
|
*/
|
||||||
void onDownloadProgress(DownloadProgressInfo progress);
|
void onDownloadProgress(DownloadProgressInfo progress);
|
||||||
}
|
}
|
||||||
|
@ -31,47 +31,47 @@ import android.os.Messenger;
|
|||||||
* should immediately call {@link #onClientUpdated}.
|
* should immediately call {@link #onClientUpdated}.
|
||||||
*/
|
*/
|
||||||
public interface IDownloaderService {
|
public interface IDownloaderService {
|
||||||
/**
|
/**
|
||||||
* Set this flag in response to the
|
* Set this flag in response to the
|
||||||
* IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
|
* IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then
|
||||||
* call RequestContinueDownload to resume a download
|
* 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
|
* Request that the service abort the current download. The service should
|
||||||
* respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}.
|
* 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
|
* Request that the service pause the current download. The service should
|
||||||
* respond by changing the state to
|
* respond by changing the state to
|
||||||
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
||||||
*/
|
*/
|
||||||
void requestPauseDownload();
|
void requestPauseDownload();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that the service continue a paused download, when in any paused
|
* Request that the service continue a paused download, when in any paused
|
||||||
* or failed state, including
|
* or failed state, including
|
||||||
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
* {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}.
|
||||||
*/
|
*/
|
||||||
void requestContinueDownload();
|
void requestContinueDownload();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the flags for this download (e.g.
|
* Set the flags for this download (e.g.
|
||||||
* {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
|
* {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}).
|
||||||
*
|
*
|
||||||
* @param flags
|
* @param flags
|
||||||
*/
|
*/
|
||||||
void setDownloadFlags(int flags);
|
void setDownloadFlags(int flags);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests that the download status be sent to the client.
|
* Requests that the download status be sent to the client.
|
||||||
*/
|
*/
|
||||||
void requestDownloadStatus();
|
void requestDownloadStatus();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this when you get {@link
|
* Call this when you get {@link
|
||||||
* IDownloaderClient.onServiceConnected(Messenger m)} from the
|
* IDownloaderClient.onServiceConnected(Messenger m)} from the
|
||||||
* DownloaderClient to register the client with the service. It will
|
* DownloaderClient to register the client with the service. It will
|
||||||
@ -79,5 +79,5 @@ public interface IDownloaderService {
|
|||||||
*
|
*
|
||||||
* @param clientMessenger
|
* @param clientMessenger
|
||||||
*/
|
*/
|
||||||
void onClientUpdated(Messenger clientMessenger);
|
void onClientUpdated(Messenger clientMessenger);
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ import android.os.Messenger;
|
|||||||
* {@link IDownloaderService#onClientUpdated}.
|
* {@link IDownloaderService#onClientUpdated}.
|
||||||
*/
|
*/
|
||||||
public interface IStub {
|
public interface IStub {
|
||||||
Messenger getMessenger();
|
Messenger getMessenger();
|
||||||
|
|
||||||
void connect(Context c);
|
void connect(Context c);
|
||||||
|
|
||||||
void disconnect(Context c);
|
void disconnect(Context c);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.google.android.vending.expansion.downloader;
|
package com.google.android.vending.expansion.downloader;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -30,94 +31,96 @@ import android.util.Log;
|
|||||||
* Contains useful helper functions, typically tied to the application context.
|
* Contains useful helper functions, typically tied to the application context.
|
||||||
*/
|
*/
|
||||||
class SystemFacade {
|
class SystemFacade {
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private NotificationManager mNotificationManager;
|
private NotificationManager mNotificationManager;
|
||||||
|
|
||||||
public SystemFacade(Context context) {
|
public SystemFacade(Context context) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mNotificationManager = (NotificationManager)
|
mNotificationManager = (NotificationManager)
|
||||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long currentTimeMillis() {
|
public long currentTimeMillis() {
|
||||||
return System.currentTimeMillis();
|
return System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getActiveNetworkType() {
|
public Integer getActiveNetworkType() {
|
||||||
ConnectivityManager connectivity =
|
ConnectivityManager connectivity =
|
||||||
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
if (connectivity == null) {
|
if (connectivity == null) {
|
||||||
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
|
@SuppressLint("MissingPermission")
|
||||||
if (activeInfo == null) {
|
NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
|
||||||
if (Constants.LOGVV) {
|
if (activeInfo == null) {
|
||||||
Log.v(Constants.TAG, "network is not available");
|
if (Constants.LOGVV) {
|
||||||
}
|
Log.v(Constants.TAG, "network is not available");
|
||||||
return null;
|
}
|
||||||
}
|
return null;
|
||||||
return activeInfo.getType();
|
}
|
||||||
}
|
return activeInfo.getType();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isNetworkRoaming() {
|
public boolean isNetworkRoaming() {
|
||||||
ConnectivityManager connectivity =
|
ConnectivityManager connectivity =
|
||||||
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
if (connectivity == null) {
|
if (connectivity == null) {
|
||||||
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
Log.w(Constants.TAG, "couldn't get connectivity manager");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkInfo info = connectivity.getActiveNetworkInfo();
|
@SuppressLint("MissingPermission")
|
||||||
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
|
NetworkInfo info = connectivity.getActiveNetworkInfo();
|
||||||
TelephonyManager tm = (TelephonyManager) mContext
|
boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
|
||||||
.getSystemService(Context.TELEPHONY_SERVICE);
|
TelephonyManager tm = (TelephonyManager)mContext
|
||||||
if (null == tm) {
|
.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
Log.w(Constants.TAG, "couldn't get telephony manager");
|
if (null == tm) {
|
||||||
return false;
|
Log.w(Constants.TAG, "couldn't get telephony manager");
|
||||||
}
|
return false;
|
||||||
boolean isRoaming = isMobile && tm.isNetworkRoaming();
|
}
|
||||||
if (Constants.LOGVV && isRoaming) {
|
boolean isRoaming = isMobile && tm.isNetworkRoaming();
|
||||||
Log.v(Constants.TAG, "network is roaming");
|
if (Constants.LOGVV && isRoaming) {
|
||||||
}
|
Log.v(Constants.TAG, "network is roaming");
|
||||||
return isRoaming;
|
}
|
||||||
}
|
return isRoaming;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getMaxBytesOverMobile() {
|
public Long getMaxBytesOverMobile() {
|
||||||
return (long) Integer.MAX_VALUE;
|
return (long)Integer.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getRecommendedMaxBytesOverMobile() {
|
public Long getRecommendedMaxBytesOverMobile() {
|
||||||
return 2097152L;
|
return 2097152L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendBroadcast(Intent intent) {
|
public void sendBroadcast(Intent intent) {
|
||||||
mContext.sendBroadcast(intent);
|
mContext.sendBroadcast(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
|
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
|
||||||
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
|
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postNotification(long id, Notification notification) {
|
public void postNotification(long id, Notification notification) {
|
||||||
/**
|
/**
|
||||||
* TODO: The system notification manager takes ints, not longs, as IDs,
|
* TODO: The system notification manager takes ints, not longs, as IDs,
|
||||||
* but the download manager uses IDs take straight from the database,
|
* but the download manager uses IDs take straight from the database,
|
||||||
* which are longs. This will have to be dealt with at some point.
|
* 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) {
|
public void cancelNotification(long id) {
|
||||||
mNotificationManager.cancel((int) id);
|
mNotificationManager.cancel((int)id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelAllNotifications() {
|
public void cancelAllNotifications() {
|
||||||
mNotificationManager.cancelAll();
|
mNotificationManager.cancelAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startThread(Thread thread) {
|
public void startThread(Thread thread) {
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,536 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of
|
|
||||||
* the SSLSessionCache added Froyo devices using reflection.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.expansion.downloader.impl;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.zip.GZIPInputStream;
|
|
||||||
import java.util.zip.GZIPOutputStream;
|
|
||||||
|
|
||||||
import org.apache.http.Header;
|
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.HttpEntityEnclosingRequest;
|
|
||||||
import org.apache.http.HttpException;
|
|
||||||
import org.apache.http.HttpHost;
|
|
||||||
import org.apache.http.HttpRequest;
|
|
||||||
import org.apache.http.HttpRequestInterceptor;
|
|
||||||
import org.apache.http.HttpResponse;
|
|
||||||
import org.apache.http.client.ClientProtocolException;
|
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.apache.http.client.ResponseHandler;
|
|
||||||
import org.apache.http.client.methods.HttpUriRequest;
|
|
||||||
import org.apache.http.client.params.HttpClientParams;
|
|
||||||
import org.apache.http.client.protocol.ClientContext;
|
|
||||||
import org.apache.http.conn.ClientConnectionManager;
|
|
||||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
|
||||||
import org.apache.http.conn.scheme.Scheme;
|
|
||||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
|
||||||
import org.apache.http.conn.scheme.SocketFactory;
|
|
||||||
import org.apache.http.conn.ssl.SSLSocketFactory;
|
|
||||||
import org.apache.http.entity.AbstractHttpEntity;
|
|
||||||
import org.apache.http.entity.ByteArrayEntity;
|
|
||||||
import org.apache.http.impl.client.DefaultHttpClient;
|
|
||||||
import org.apache.http.impl.client.RequestWrapper;
|
|
||||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
|
||||||
import org.apache.http.params.BasicHttpParams;
|
|
||||||
import org.apache.http.params.HttpConnectionParams;
|
|
||||||
import org.apache.http.params.HttpParams;
|
|
||||||
import org.apache.http.params.HttpProtocolParams;
|
|
||||||
import org.apache.http.protocol.BasicHttpContext;
|
|
||||||
import org.apache.http.protocol.BasicHttpProcessor;
|
|
||||||
import org.apache.http.protocol.HttpContext;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.SSLCertificateSocketFactory;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subclass of the Apache {@link DefaultHttpClient} that is configured with
|
|
||||||
* reasonable default settings and registered schemes for Android, and
|
|
||||||
* also lets the user add {@link HttpRequestInterceptor} classes.
|
|
||||||
* Don't create this directly, use the {@link #newInstance} factory method.
|
|
||||||
*
|
|
||||||
* <p>This client processes cookies but does not retain them by default.
|
|
||||||
* To retain cookies, simply add a cookie store to the HttpContext:</p>
|
|
||||||
*
|
|
||||||
* <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
|
|
||||||
*/
|
|
||||||
public final class AndroidHttpClient implements HttpClient {
|
|
||||||
|
|
||||||
static Class<?> sSslSessionCacheClass;
|
|
||||||
static {
|
|
||||||
// if we are on Froyo+ devices, we can take advantage of the SSLSessionCache
|
|
||||||
try {
|
|
||||||
sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache");
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gzip of data shorter than this probably won't be worthwhile
|
|
||||||
public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
|
|
||||||
|
|
||||||
// Default connection and socket timeout of 60 seconds. Tweak to taste.
|
|
||||||
private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000;
|
|
||||||
|
|
||||||
private static final String TAG = "AndroidHttpClient";
|
|
||||||
|
|
||||||
|
|
||||||
/** Interceptor throws an exception if the executing thread is blocked */
|
|
||||||
private static final HttpRequestInterceptor sThreadCheckInterceptor =
|
|
||||||
new HttpRequestInterceptor() {
|
|
||||||
public void process(HttpRequest request, HttpContext context) {
|
|
||||||
// Prevent the HttpRequest from being sent on the main thread
|
|
||||||
if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) {
|
|
||||||
throw new RuntimeException("This thread forbids HTTP requests");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new HttpClient with reasonable defaults (which you can update).
|
|
||||||
*
|
|
||||||
* @param userAgent to report in your HTTP requests
|
|
||||||
* @param context to use for caching SSL sessions (may be null for no caching)
|
|
||||||
* @return AndroidHttpClient for you to use for all your requests.
|
|
||||||
*/
|
|
||||||
public static AndroidHttpClient newInstance(String userAgent, Context context) {
|
|
||||||
HttpParams params = new BasicHttpParams();
|
|
||||||
|
|
||||||
// Turn off stale checking. Our connections break all the time anyway,
|
|
||||||
// and it's not worth it to pay the penalty of checking every time.
|
|
||||||
HttpConnectionParams.setStaleCheckingEnabled(params, false);
|
|
||||||
|
|
||||||
HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT);
|
|
||||||
HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT);
|
|
||||||
HttpConnectionParams.setSocketBufferSize(params, 8192);
|
|
||||||
|
|
||||||
// Don't handle redirects -- return them to the caller. Our code
|
|
||||||
// often wants to re-POST after a redirect, which we must do ourselves.
|
|
||||||
HttpClientParams.setRedirecting(params, false);
|
|
||||||
|
|
||||||
Object sessionCache = null;
|
|
||||||
// Use a session cache for SSL sockets -- Froyo only
|
|
||||||
if ( null != context && null != sSslSessionCacheClass ) {
|
|
||||||
Constructor<?> ct;
|
|
||||||
try {
|
|
||||||
ct = sSslSessionCacheClass.getConstructor(Context.class);
|
|
||||||
sessionCache = ct.newInstance(context);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InstantiationException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the specified user agent and register standard protocols.
|
|
||||||
HttpProtocolParams.setUserAgent(params, userAgent);
|
|
||||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
|
||||||
schemeRegistry.register(new Scheme("http",
|
|
||||||
PlainSocketFactory.getSocketFactory(), 80));
|
|
||||||
SocketFactory sslCertificateSocketFactory = null;
|
|
||||||
if ( null != sessionCache ) {
|
|
||||||
Method getHttpSocketFactoryMethod;
|
|
||||||
try {
|
|
||||||
getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass);
|
|
||||||
sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( null == sslCertificateSocketFactory ) {
|
|
||||||
sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory();
|
|
||||||
}
|
|
||||||
schemeRegistry.register(new Scheme("https",
|
|
||||||
sslCertificateSocketFactory, 443));
|
|
||||||
|
|
||||||
ClientConnectionManager manager =
|
|
||||||
new ThreadSafeClientConnManager(params, schemeRegistry);
|
|
||||||
|
|
||||||
// We use a factory method to modify superclass initialization
|
|
||||||
// parameters without the funny call-a-static-method dance.
|
|
||||||
return new AndroidHttpClient(manager, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new HttpClient with reasonable defaults (which you can update).
|
|
||||||
* @param userAgent to report in your HTTP requests.
|
|
||||||
* @return AndroidHttpClient for you to use for all your requests.
|
|
||||||
*/
|
|
||||||
public static AndroidHttpClient newInstance(String userAgent) {
|
|
||||||
return newInstance(userAgent, null /* session cache */);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final HttpClient delegate;
|
|
||||||
|
|
||||||
private RuntimeException mLeakedException = new IllegalStateException(
|
|
||||||
"AndroidHttpClient created and never closed");
|
|
||||||
|
|
||||||
private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
|
|
||||||
this.delegate = new DefaultHttpClient(ccm, params) {
|
|
||||||
@Override
|
|
||||||
protected BasicHttpProcessor createHttpProcessor() {
|
|
||||||
// Add interceptor to prevent making requests from main thread.
|
|
||||||
BasicHttpProcessor processor = super.createHttpProcessor();
|
|
||||||
processor.addRequestInterceptor(sThreadCheckInterceptor);
|
|
||||||
processor.addRequestInterceptor(new CurlLogger());
|
|
||||||
|
|
||||||
return processor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected HttpContext createHttpContext() {
|
|
||||||
// Same as DefaultHttpClient.createHttpContext() minus the
|
|
||||||
// cookie store.
|
|
||||||
HttpContext context = new BasicHttpContext();
|
|
||||||
context.setAttribute(
|
|
||||||
ClientContext.AUTHSCHEME_REGISTRY,
|
|
||||||
getAuthSchemes());
|
|
||||||
context.setAttribute(
|
|
||||||
ClientContext.COOKIESPEC_REGISTRY,
|
|
||||||
getCookieSpecs());
|
|
||||||
context.setAttribute(
|
|
||||||
ClientContext.CREDS_PROVIDER,
|
|
||||||
getCredentialsProvider());
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void finalize() throws Throwable {
|
|
||||||
super.finalize();
|
|
||||||
if (mLeakedException != null) {
|
|
||||||
Log.e(TAG, "Leak found", mLeakedException);
|
|
||||||
mLeakedException = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies a request to indicate to the server that we would like a
|
|
||||||
* gzipped response. (Uses the "Accept-Encoding" HTTP header.)
|
|
||||||
* @param request the request to modify
|
|
||||||
* @see #getUngzippedContent
|
|
||||||
*/
|
|
||||||
public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
|
|
||||||
request.addHeader("Accept-Encoding", "gzip");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the input stream from a response entity. If the entity is gzipped
|
|
||||||
* then this will get a stream over the uncompressed data.
|
|
||||||
*
|
|
||||||
* @param entity the entity whose content should be read
|
|
||||||
* @return the input stream to read from
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public static InputStream getUngzippedContent(HttpEntity entity)
|
|
||||||
throws IOException {
|
|
||||||
InputStream responseStream = entity.getContent();
|
|
||||||
if (responseStream == null) return responseStream;
|
|
||||||
Header header = entity.getContentEncoding();
|
|
||||||
if (header == null) return responseStream;
|
|
||||||
String contentEncoding = header.getValue();
|
|
||||||
if (contentEncoding == null) return responseStream;
|
|
||||||
if (contentEncoding.contains("gzip")) responseStream
|
|
||||||
= new GZIPInputStream(responseStream);
|
|
||||||
return responseStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release resources associated with this client. You must call this,
|
|
||||||
* or significant resources (sockets and memory) may be leaked.
|
|
||||||
*/
|
|
||||||
public void close() {
|
|
||||||
if (mLeakedException != null) {
|
|
||||||
getConnectionManager().shutdown();
|
|
||||||
mLeakedException = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpParams getParams() {
|
|
||||||
return delegate.getParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientConnectionManager getConnectionManager() {
|
|
||||||
return delegate.getConnectionManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponse execute(HttpUriRequest request) throws IOException {
|
|
||||||
return delegate.execute(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponse execute(HttpUriRequest request, HttpContext context)
|
|
||||||
throws IOException {
|
|
||||||
return delegate.execute(request, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponse execute(HttpHost target, HttpRequest request)
|
|
||||||
throws IOException {
|
|
||||||
return delegate.execute(target, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponse execute(HttpHost target, HttpRequest request,
|
|
||||||
HttpContext context) throws IOException {
|
|
||||||
return delegate.execute(target, request, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T execute(HttpUriRequest request,
|
|
||||||
ResponseHandler<? extends T> responseHandler)
|
|
||||||
throws IOException, ClientProtocolException {
|
|
||||||
return delegate.execute(request, responseHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T execute(HttpUriRequest request,
|
|
||||||
ResponseHandler<? extends T> responseHandler, HttpContext context)
|
|
||||||
throws IOException, ClientProtocolException {
|
|
||||||
return delegate.execute(request, responseHandler, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T execute(HttpHost target, HttpRequest request,
|
|
||||||
ResponseHandler<? extends T> responseHandler) throws IOException,
|
|
||||||
ClientProtocolException {
|
|
||||||
return delegate.execute(target, request, responseHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T execute(HttpHost target, HttpRequest request,
|
|
||||||
ResponseHandler<? extends T> responseHandler, HttpContext context)
|
|
||||||
throws IOException, ClientProtocolException {
|
|
||||||
return delegate.execute(target, request, responseHandler, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compress data to send to server.
|
|
||||||
* Creates a Http Entity holding the gzipped data.
|
|
||||||
* The data will not be compressed if it is too short.
|
|
||||||
* @param data The bytes to compress
|
|
||||||
* @return Entity holding the data
|
|
||||||
*/
|
|
||||||
public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
|
|
||||||
throws IOException {
|
|
||||||
AbstractHttpEntity entity;
|
|
||||||
if (data.length < getMinGzipSize(resolver)) {
|
|
||||||
entity = new ByteArrayEntity(data);
|
|
||||||
} else {
|
|
||||||
ByteArrayOutputStream arr = new ByteArrayOutputStream();
|
|
||||||
OutputStream zipper = new GZIPOutputStream(arr);
|
|
||||||
zipper.write(data);
|
|
||||||
zipper.close();
|
|
||||||
entity = new ByteArrayEntity(arr.toByteArray());
|
|
||||||
entity.setContentEncoding("gzip");
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the minimum size for compressing data.
|
|
||||||
* Shorter data will not be compressed.
|
|
||||||
*/
|
|
||||||
public static long getMinGzipSize(ContentResolver resolver) {
|
|
||||||
return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant.
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cURL logging support. */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logging tag and level.
|
|
||||||
*/
|
|
||||||
private static class LoggingConfiguration {
|
|
||||||
|
|
||||||
private final String tag;
|
|
||||||
private final int level;
|
|
||||||
|
|
||||||
private LoggingConfiguration(String tag, int level) {
|
|
||||||
this.tag = tag;
|
|
||||||
this.level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if logging is turned on for this configuration.
|
|
||||||
*/
|
|
||||||
private boolean isLoggable() {
|
|
||||||
return Log.isLoggable(tag, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prints a message using this configuration.
|
|
||||||
*/
|
|
||||||
private void println(String message) {
|
|
||||||
Log.println(level, tag, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** cURL logging configuration. */
|
|
||||||
private volatile LoggingConfiguration curlConfiguration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables cURL request logging for this client.
|
|
||||||
*
|
|
||||||
* @param name to log messages with
|
|
||||||
* @param level at which to log messages (see {@link android.util.Log})
|
|
||||||
*/
|
|
||||||
public void enableCurlLogging(String name, int level) {
|
|
||||||
if (name == null) {
|
|
||||||
throw new NullPointerException("name");
|
|
||||||
}
|
|
||||||
if (level < Log.VERBOSE || level > Log.ASSERT) {
|
|
||||||
throw new IllegalArgumentException("Level is out of range ["
|
|
||||||
+ Log.VERBOSE + ".." + Log.ASSERT + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
curlConfiguration = new LoggingConfiguration(name, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables cURL logging for this client.
|
|
||||||
*/
|
|
||||||
public void disableCurlLogging() {
|
|
||||||
curlConfiguration = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs cURL commands equivalent to requests.
|
|
||||||
*/
|
|
||||||
private class CurlLogger implements HttpRequestInterceptor {
|
|
||||||
public void process(HttpRequest request, HttpContext context)
|
|
||||||
throws HttpException, IOException {
|
|
||||||
LoggingConfiguration configuration = curlConfiguration;
|
|
||||||
if (configuration != null
|
|
||||||
&& configuration.isLoggable()
|
|
||||||
&& request instanceof HttpUriRequest) {
|
|
||||||
// Never print auth token -- we used to check ro.secure=0 to
|
|
||||||
// enable that, but can't do that in unbundled code.
|
|
||||||
configuration.println(toCurl((HttpUriRequest) request, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a cURL command equivalent to the given request.
|
|
||||||
*/
|
|
||||||
private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
|
|
||||||
builder.append("curl ");
|
|
||||||
|
|
||||||
for (Header header: request.getAllHeaders()) {
|
|
||||||
if (!logAuthToken
|
|
||||||
&& (header.getName().equals("Authorization") ||
|
|
||||||
header.getName().equals("Cookie"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
builder.append("--header \"");
|
|
||||||
builder.append(header.toString().trim());
|
|
||||||
builder.append("\" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
URI uri = request.getURI();
|
|
||||||
|
|
||||||
// If this is a wrapped request, use the URI from the original
|
|
||||||
// request instead. getURI() on the wrapper seems to return a
|
|
||||||
// relative URI. We want an absolute URI.
|
|
||||||
if (request instanceof RequestWrapper) {
|
|
||||||
HttpRequest original = ((RequestWrapper) request).getOriginal();
|
|
||||||
if (original instanceof HttpUriRequest) {
|
|
||||||
uri = ((HttpUriRequest) original).getURI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.append("\"");
|
|
||||||
builder.append(uri);
|
|
||||||
builder.append("\"");
|
|
||||||
|
|
||||||
if (request instanceof HttpEntityEnclosingRequest) {
|
|
||||||
HttpEntityEnclosingRequest entityRequest =
|
|
||||||
(HttpEntityEnclosingRequest) request;
|
|
||||||
HttpEntity entity = entityRequest.getEntity();
|
|
||||||
if (entity != null && entity.isRepeatable()) {
|
|
||||||
if (entity.getContentLength() < 1024) {
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
entity.writeTo(stream);
|
|
||||||
String entityString = stream.toString();
|
|
||||||
|
|
||||||
// TODO: Check the content type, too.
|
|
||||||
builder.append(" --data-ascii \"")
|
|
||||||
.append(entityString)
|
|
||||||
.append("\"");
|
|
||||||
} else {
|
|
||||||
builder.append(" [TOO MUCH DATA TO INCLUDE]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the date of the given HTTP date string. This method can identify
|
|
||||||
* and parse the date formats emitted by common HTTP servers, such as
|
|
||||||
* <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>,
|
|
||||||
* <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>,
|
|
||||||
* <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>,
|
|
||||||
* <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and
|
|
||||||
* <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI
|
|
||||||
* C's asctime()</a>.
|
|
||||||
*
|
|
||||||
* @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
|
|
||||||
* @throws IllegalArgumentException if {@code dateString} is not a date or
|
|
||||||
* of an unsupported format.
|
|
||||||
*/
|
|
||||||
public static long parseDate(String dateString) {
|
|
||||||
return HttpDateTime.parse(dateString);
|
|
||||||
}
|
|
||||||
}
|
|
@ -32,81 +32,80 @@ import android.util.Log;
|
|||||||
* intent, it does not queue up batches of intents of the same type.
|
* intent, it does not queue up batches of intents of the same type.
|
||||||
*/
|
*/
|
||||||
public abstract class CustomIntentService extends Service {
|
public abstract class CustomIntentService extends Service {
|
||||||
private String mName;
|
private String mName;
|
||||||
private boolean mRedelivery;
|
private boolean mRedelivery;
|
||||||
private volatile ServiceHandler mServiceHandler;
|
private volatile ServiceHandler mServiceHandler;
|
||||||
private volatile Looper mServiceLooper;
|
private volatile Looper mServiceLooper;
|
||||||
private static final String LOG_TAG = "CancellableIntentService";
|
private static final String LOG_TAG = "CustomIntentService";
|
||||||
private static final int WHAT_MESSAGE = -10;
|
private static final int WHAT_MESSAGE = -10;
|
||||||
|
|
||||||
public CustomIntentService(String paramString) {
|
public CustomIntentService(String paramString) {
|
||||||
this.mName = paramString;
|
this.mName = paramString;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent paramIntent) {
|
public IBinder onBind(Intent paramIntent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
HandlerThread localHandlerThread = new HandlerThread("IntentService["
|
HandlerThread localHandlerThread = new HandlerThread("IntentService[" + this.mName + "]");
|
||||||
+ this.mName + "]");
|
localHandlerThread.start();
|
||||||
localHandlerThread.start();
|
this.mServiceLooper = localHandlerThread.getLooper();
|
||||||
this.mServiceLooper = localHandlerThread.getLooper();
|
this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
|
||||||
this.mServiceHandler = new ServiceHandler(this.mServiceLooper);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
Thread localThread = this.mServiceLooper.getThread();
|
Thread localThread = this.mServiceLooper.getThread();
|
||||||
if ((localThread != null) && (localThread.isAlive())) {
|
if ((localThread != null) && (localThread.isAlive())) {
|
||||||
localThread.interrupt();
|
localThread.interrupt();
|
||||||
}
|
}
|
||||||
this.mServiceLooper.quit();
|
this.mServiceLooper.quit();
|
||||||
Log.d(LOG_TAG, "onDestroy");
|
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
|
@Override
|
||||||
public void onStart(Intent paramIntent, int startId) {
|
public void onStart(Intent paramIntent, int startId) {
|
||||||
if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
|
if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) {
|
||||||
Message localMessage = this.mServiceHandler.obtainMessage();
|
Message localMessage = this.mServiceHandler.obtainMessage();
|
||||||
localMessage.arg1 = startId;
|
localMessage.arg1 = startId;
|
||||||
localMessage.obj = paramIntent;
|
localMessage.obj = paramIntent;
|
||||||
localMessage.what = WHAT_MESSAGE;
|
localMessage.what = WHAT_MESSAGE;
|
||||||
this.mServiceHandler.sendMessage(localMessage);
|
this.mServiceHandler.sendMessage(localMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent paramIntent, int flags, int startId) {
|
public int onStartCommand(Intent paramIntent, int flags, int startId) {
|
||||||
onStart(paramIntent, startId);
|
onStart(paramIntent, startId);
|
||||||
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
|
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIntentRedelivery(boolean enabled) {
|
public void setIntentRedelivery(boolean enabled) {
|
||||||
this.mRedelivery = enabled;
|
this.mRedelivery = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ServiceHandler extends Handler {
|
private final class ServiceHandler extends Handler {
|
||||||
public ServiceHandler(Looper looper) {
|
public ServiceHandler(Looper looper) {
|
||||||
super(looper);
|
super(looper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message paramMessage) {
|
public void handleMessage(Message paramMessage) {
|
||||||
CustomIntentService.this
|
CustomIntentService.this
|
||||||
.onHandleIntent((Intent) paramMessage.obj);
|
.onHandleIntent((Intent)paramMessage.obj);
|
||||||
if (shouldStop()) {
|
if (shouldStop()) {
|
||||||
Log.d(LOG_TAG, "stopSelf");
|
Log.d(LOG_TAG, "stopSelf");
|
||||||
CustomIntentService.this.stopSelf(paramMessage.arg1);
|
CustomIntentService.this.stopSelf(paramMessage.arg1);
|
||||||
Log.d(LOG_TAG, "afterStopSelf");
|
Log.d(LOG_TAG, "afterStopSelf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.expansion.downloader.impl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses the class-loader model to utilize the updated notification builders in
|
|
||||||
* Honeycomb while maintaining a compatible version for older devices.
|
|
||||||
*/
|
|
||||||
public class CustomNotificationFactory {
|
|
||||||
static public DownloadNotification.ICustomNotification createCustomNotification() {
|
|
||||||
if (android.os.Build.VERSION.SDK_INT > 13)
|
|
||||||
return new V14CustomNotification();
|
|
||||||
else
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,68 +25,68 @@ import android.util.Log;
|
|||||||
* Representation of information about an individual download from the database.
|
* Representation of information about an individual download from the database.
|
||||||
*/
|
*/
|
||||||
public class DownloadInfo {
|
public class DownloadInfo {
|
||||||
public String mUri;
|
public String mUri;
|
||||||
public final int mIndex;
|
public final int mIndex;
|
||||||
public final String mFileName;
|
public final String mFileName;
|
||||||
public String mETag;
|
public String mETag;
|
||||||
public long mTotalBytes;
|
public long mTotalBytes;
|
||||||
public long mCurrentBytes;
|
public long mCurrentBytes;
|
||||||
public long mLastMod;
|
public long mLastMod;
|
||||||
public int mStatus;
|
public int mStatus;
|
||||||
public int mControl;
|
public int mControl;
|
||||||
public int mNumFailed;
|
public int mNumFailed;
|
||||||
public int mRetryAfter;
|
public int mRetryAfter;
|
||||||
public int mRedirectCount;
|
public int mRedirectCount;
|
||||||
|
|
||||||
boolean mInitialized;
|
boolean mInitialized;
|
||||||
|
|
||||||
public int mFuzz;
|
public int mFuzz;
|
||||||
|
|
||||||
public DownloadInfo(int index, String fileName, String pkg) {
|
public DownloadInfo(int index, String fileName, String pkg) {
|
||||||
mFuzz = Helpers.sRandom.nextInt(1001);
|
mFuzz = Helpers.sRandom.nextInt(1001);
|
||||||
mFileName = fileName;
|
mFileName = fileName;
|
||||||
mIndex = index;
|
mIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetDownload() {
|
public void resetDownload() {
|
||||||
mCurrentBytes = 0;
|
mCurrentBytes = 0;
|
||||||
mETag = "";
|
mETag = "";
|
||||||
mLastMod = 0;
|
mLastMod = 0;
|
||||||
mStatus = 0;
|
mStatus = 0;
|
||||||
mControl = 0;
|
mControl = 0;
|
||||||
mNumFailed = 0;
|
mNumFailed = 0;
|
||||||
mRetryAfter = 0;
|
mRetryAfter = 0;
|
||||||
mRedirectCount = 0;
|
mRedirectCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the time when a download should be restarted.
|
* Returns the time when a download should be restarted.
|
||||||
*/
|
*/
|
||||||
public long restartTime(long now) {
|
public long restartTime(long now) {
|
||||||
if (mNumFailed == 0) {
|
if (mNumFailed == 0) {
|
||||||
return now;
|
return now;
|
||||||
}
|
}
|
||||||
if (mRetryAfter > 0) {
|
if (mRetryAfter > 0) {
|
||||||
return mLastMod + mRetryAfter;
|
return mLastMod + mRetryAfter;
|
||||||
}
|
}
|
||||||
return mLastMod +
|
return mLastMod +
|
||||||
Constants.RETRY_FIRST_DELAY *
|
Constants.RETRY_FIRST_DELAY *
|
||||||
(1000 + mFuzz) * (1 << (mNumFailed - 1));
|
(1000 + mFuzz) * (1 << (mNumFailed - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logVerboseInfo() {
|
public void logVerboseInfo() {
|
||||||
Log.v(Constants.TAG, "Service adding new entry");
|
Log.v(Constants.TAG, "Service adding new entry");
|
||||||
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
||||||
Log.v(Constants.TAG, "URI : " + mUri);
|
Log.v(Constants.TAG, "URI : " + mUri);
|
||||||
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
Log.v(Constants.TAG, "FILENAME: " + mFileName);
|
||||||
Log.v(Constants.TAG, "CONTROL : " + mControl);
|
Log.v(Constants.TAG, "CONTROL : " + mControl);
|
||||||
Log.v(Constants.TAG, "STATUS : " + mStatus);
|
Log.v(Constants.TAG, "STATUS : " + mStatus);
|
||||||
Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
|
Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
|
||||||
Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
|
Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
|
||||||
Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
|
Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount);
|
||||||
Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
|
Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
|
||||||
Log.v(Constants.TAG, "TOTAL : " + mTotalBytes);
|
Log.v(Constants.TAG, "TOTAL : " + mTotalBytes);
|
||||||
Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
|
Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
|
||||||
Log.v(Constants.TAG, "ETAG : " + mETag);
|
Log.v(Constants.TAG, "ETAG : " + mETag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,12 @@ import com.google.android.vending.expansion.downloader.DownloaderClientMarshalle
|
|||||||
import com.google.android.vending.expansion.downloader.Helpers;
|
import com.google.android.vending.expansion.downloader.Helpers;
|
||||||
import com.google.android.vending.expansion.downloader.IDownloaderClient;
|
import com.google.android.vending.expansion.downloader.IDownloaderClient;
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Messenger;
|
import android.os.Messenger;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class handles displaying the notification associated with the download
|
* This class handles displaying the notification associated with the download
|
||||||
@ -41,190 +42,183 @@ import android.os.Messenger;
|
|||||||
*/
|
*/
|
||||||
public class DownloadNotification implements IDownloaderClient {
|
public class DownloadNotification implements IDownloaderClient {
|
||||||
|
|
||||||
private int mState;
|
private int mState;
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final NotificationManager mNotificationManager;
|
private final NotificationManager mNotificationManager;
|
||||||
private String mCurrentTitle;
|
private CharSequence mCurrentTitle;
|
||||||
|
|
||||||
private IDownloaderClient mClientProxy;
|
private IDownloaderClient mClientProxy;
|
||||||
final ICustomNotification mCustomNotification;
|
private NotificationCompat.Builder mActiveDownloadBuilder;
|
||||||
private Notification.Builder mNotificationBuilder;
|
private NotificationCompat.Builder mBuilder;
|
||||||
private Notification.Builder mCurrentNotificationBuilder;
|
private NotificationCompat.Builder mCurrentBuilder;
|
||||||
private CharSequence mLabel;
|
private CharSequence mLabel;
|
||||||
private String mCurrentText;
|
private String mCurrentText;
|
||||||
private PendingIntent mContentIntent;
|
private DownloadProgressInfo mProgressInfo;
|
||||||
private DownloadProgressInfo mProgressInfo;
|
private PendingIntent mContentIntent;
|
||||||
|
|
||||||
static final String LOGTAG = "DownloadNotification";
|
static final String LOGTAG = "DownloadNotification";
|
||||||
static final int NOTIFICATION_ID = LOGTAG.hashCode();
|
static final int NOTIFICATION_ID = LOGTAG.hashCode();
|
||||||
|
|
||||||
public PendingIntent getClientIntent() {
|
public PendingIntent getClientIntent() {
|
||||||
return mContentIntent;
|
return mContentIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClientIntent(PendingIntent mClientIntent) {
|
public void setClientIntent(PendingIntent clientIntent) {
|
||||||
this.mContentIntent = mClientIntent;
|
this.mBuilder.setContentIntent(clientIntent);
|
||||||
}
|
this.mActiveDownloadBuilder.setContentIntent(clientIntent);
|
||||||
|
this.mContentIntent = clientIntent;
|
||||||
|
}
|
||||||
|
|
||||||
public void resendState() {
|
public void resendState() {
|
||||||
if (null != mClientProxy) {
|
if (null != mClientProxy) {
|
||||||
mClientProxy.onDownloadStateChanged(mState);
|
mClientProxy.onDownloadStateChanged(mState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownloadStateChanged(int newState) {
|
public void onDownloadStateChanged(int newState) {
|
||||||
if (null != mClientProxy) {
|
if (null != mClientProxy) {
|
||||||
mClientProxy.onDownloadStateChanged(newState);
|
mClientProxy.onDownloadStateChanged(newState);
|
||||||
}
|
}
|
||||||
if (newState != mState) {
|
if (newState != mState) {
|
||||||
mState = newState;
|
mState = newState;
|
||||||
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
|
if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int stringDownloadID;
|
int stringDownloadID;
|
||||||
int iconResource;
|
int iconResource;
|
||||||
boolean ongoingEvent;
|
boolean ongoingEvent;
|
||||||
|
|
||||||
// get the new title string and paused text
|
// get the new title string and paused text
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case 0:
|
case 0:
|
||||||
iconResource = android.R.drawable.stat_sys_warning;
|
iconResource = android.R.drawable.stat_sys_warning;
|
||||||
stringDownloadID = R.string.state_unknown;
|
stringDownloadID = R.string.state_unknown;
|
||||||
ongoingEvent = false;
|
ongoingEvent = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IDownloaderClient.STATE_DOWNLOADING:
|
case IDownloaderClient.STATE_DOWNLOADING:
|
||||||
iconResource = android.R.drawable.stat_sys_download;
|
iconResource = android.R.drawable.stat_sys_download;
|
||||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||||
ongoingEvent = true;
|
ongoingEvent = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IDownloaderClient.STATE_FETCHING_URL:
|
case IDownloaderClient.STATE_FETCHING_URL:
|
||||||
case IDownloaderClient.STATE_CONNECTING:
|
case IDownloaderClient.STATE_CONNECTING:
|
||||||
iconResource = android.R.drawable.stat_sys_download_done;
|
iconResource = android.R.drawable.stat_sys_download_done;
|
||||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||||
ongoingEvent = true;
|
ongoingEvent = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IDownloaderClient.STATE_COMPLETED:
|
case IDownloaderClient.STATE_COMPLETED:
|
||||||
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
|
||||||
iconResource = android.R.drawable.stat_sys_download_done;
|
iconResource = android.R.drawable.stat_sys_download_done;
|
||||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||||
ongoingEvent = false;
|
ongoingEvent = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IDownloaderClient.STATE_FAILED:
|
case IDownloaderClient.STATE_FAILED:
|
||||||
case IDownloaderClient.STATE_FAILED_CANCELED:
|
case IDownloaderClient.STATE_FAILED_CANCELED:
|
||||||
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
case IDownloaderClient.STATE_FAILED_FETCHING_URL:
|
||||||
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
case IDownloaderClient.STATE_FAILED_SDCARD_FULL:
|
||||||
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
case IDownloaderClient.STATE_FAILED_UNLICENSED:
|
||||||
iconResource = android.R.drawable.stat_sys_warning;
|
iconResource = android.R.drawable.stat_sys_warning;
|
||||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||||
ongoingEvent = false;
|
ongoingEvent = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
iconResource = android.R.drawable.stat_sys_warning;
|
iconResource = android.R.drawable.stat_sys_warning;
|
||||||
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState);
|
||||||
ongoingEvent = true;
|
ongoingEvent = true;
|
||||||
break;
|
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
|
mCurrentText = mContext.getString(stringDownloadID);
|
||||||
public void onDownloadProgress(DownloadProgressInfo progress) {
|
mCurrentTitle = mLabel;
|
||||||
mProgressInfo = progress;
|
mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
|
||||||
if (null != mClientProxy) {
|
mCurrentBuilder.setSmallIcon(iconResource);
|
||||||
mClientProxy.onDownloadProgress(progress);
|
mCurrentBuilder.setContentTitle(mCurrentTitle);
|
||||||
}
|
mCurrentBuilder.setContentText(mCurrentText);
|
||||||
if (progress.mOverallTotal <= 0) {
|
if (ongoingEvent) {
|
||||||
// we just show the text
|
mCurrentBuilder.setOngoing(true);
|
||||||
mNotificationBuilder.setTicker(mCurrentTitle);
|
} else {
|
||||||
mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
mCurrentBuilder.setOngoing(false);
|
||||||
mNotificationBuilder.setContentTitle(mCurrentTitle);
|
mCurrentBuilder.setAutoCancel(true);
|
||||||
mNotificationBuilder.setContentText(mCurrentText);
|
}
|
||||||
mNotificationBuilder.setContentIntent(mContentIntent);
|
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
|
||||||
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 {
|
@Override
|
||||||
void setTitle(CharSequence title);
|
public void onDownloadProgress(DownloadProgressInfo progress) {
|
||||||
|
mProgressInfo = progress;
|
||||||
|
if (null != mClientProxy) {
|
||||||
|
mClientProxy.onDownloadProgress(progress);
|
||||||
|
}
|
||||||
|
if (progress.mOverallTotal <= 0) {
|
||||||
|
// we just show the text
|
||||||
|
mBuilder.setTicker(mCurrentTitle);
|
||||||
|
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||||
|
mBuilder.setContentTitle(mCurrentTitle);
|
||||||
|
mBuilder.setContentText(mCurrentText);
|
||||||
|
mCurrentBuilder = mBuilder;
|
||||||
|
} else {
|
||||||
|
mActiveDownloadBuilder.setProgress((int)progress.mOverallTotal, (int)progress.mOverallProgress, false);
|
||||||
|
mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
|
||||||
|
mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||||
|
mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
|
||||||
|
mActiveDownloadBuilder.setContentTitle(mLabel);
|
||||||
|
mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification,
|
||||||
|
Helpers.getTimeRemaining(progress.mTimeRemaining)));
|
||||||
|
mCurrentBuilder = mActiveDownloadBuilder;
|
||||||
|
}
|
||||||
|
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
void setTicker(CharSequence ticker);
|
/**
|
||||||
|
|
||||||
void setPendingIntent(PendingIntent mContentIntent);
|
|
||||||
|
|
||||||
void setTotalBytes(long totalBytes);
|
|
||||||
|
|
||||||
void setCurrentBytes(long currentBytes);
|
|
||||||
|
|
||||||
void setIcon(int iconResource);
|
|
||||||
|
|
||||||
void setTimeRemaining(long timeRemaining);
|
|
||||||
|
|
||||||
Notification.Builder updateNotification(Context c);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called in response to onClientUpdated. Creates a new proxy and notifies
|
* Called in response to onClientUpdated. Creates a new proxy and notifies
|
||||||
* it of the current state.
|
* it of the current state.
|
||||||
*
|
*
|
||||||
* @param msg the client Messenger to notify
|
* @param msg the client Messenger to notify
|
||||||
*/
|
*/
|
||||||
public void setMessenger(Messenger msg) {
|
public void setMessenger(Messenger msg) {
|
||||||
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
|
mClientProxy = DownloaderClientMarshaller.CreateProxy(msg);
|
||||||
if (null != mProgressInfo) {
|
if (null != mProgressInfo) {
|
||||||
mClientProxy.onDownloadProgress(mProgressInfo);
|
mClientProxy.onDownloadProgress(mProgressInfo);
|
||||||
}
|
}
|
||||||
if (mState != -1) {
|
if (mState != -1) {
|
||||||
mClientProxy.onDownloadStateChanged(mState);
|
mClientProxy.onDownloadStateChanged(mState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param ctx The context to use to obtain access to the Notification
|
* @param ctx The context to use to obtain access to the Notification
|
||||||
* Service
|
* Service
|
||||||
*/
|
*/
|
||||||
DownloadNotification(Context ctx, CharSequence applicationLabel) {
|
DownloadNotification(Context ctx, CharSequence applicationLabel) {
|
||||||
mState = -1;
|
mState = -1;
|
||||||
mContext = ctx;
|
mContext = ctx;
|
||||||
mLabel = applicationLabel;
|
mLabel = applicationLabel;
|
||||||
mNotificationManager = (NotificationManager)
|
mNotificationManager = (NotificationManager)
|
||||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
mCustomNotification = CustomNotificationFactory
|
mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
|
||||||
.createCustomNotification();
|
mBuilder = new NotificationCompat.Builder(ctx);
|
||||||
mNotificationBuilder = new Notification.Builder(ctx);
|
|
||||||
mCurrentNotificationBuilder = mNotificationBuilder;
|
|
||||||
|
|
||||||
}
|
// Set Notification category and priorities to something that makes sense for a long
|
||||||
|
// lived background task.
|
||||||
|
mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||||
|
mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
|
||||||
|
|
||||||
@Override
|
mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||||
public void onServiceConnected(Messenger m) {
|
mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
|
||||||
}
|
|
||||||
|
|
||||||
|
mCurrentBuilder = mBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(Messenger m) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,484 +27,443 @@ import android.provider.BaseColumns;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
public class DownloadsDB {
|
public class DownloadsDB {
|
||||||
private static final String DATABASE_NAME = "DownloadsDB";
|
private static final String DATABASE_NAME = "DownloadsDB";
|
||||||
private static final int DATABASE_VERSION = 7;
|
private static final int DATABASE_VERSION = 7;
|
||||||
public static final String LOG_TAG = DownloadsDB.class.getName();
|
public static final String LOG_TAG = DownloadsDB.class.getName();
|
||||||
final SQLiteOpenHelper mHelper;
|
final SQLiteOpenHelper mHelper;
|
||||||
SQLiteStatement mGetDownloadByIndex;
|
SQLiteStatement mGetDownloadByIndex;
|
||||||
SQLiteStatement mUpdateCurrentBytes;
|
SQLiteStatement mUpdateCurrentBytes;
|
||||||
private static DownloadsDB mDownloadsDB;
|
private static DownloadsDB mDownloadsDB;
|
||||||
long mMetadataRowID = -1;
|
long mMetadataRowID = -1;
|
||||||
int mVersionCode = -1;
|
int mVersionCode = -1;
|
||||||
int mStatus = -1;
|
int mStatus = -1;
|
||||||
int mFlags;
|
int mFlags;
|
||||||
|
|
||||||
static public synchronized DownloadsDB getDB(Context paramContext) {
|
static public synchronized DownloadsDB getDB(Context paramContext) {
|
||||||
if (null == mDownloadsDB) {
|
if (null == mDownloadsDB) {
|
||||||
return new DownloadsDB(paramContext);
|
return new DownloadsDB(paramContext);
|
||||||
}
|
}
|
||||||
return mDownloadsDB;
|
return mDownloadsDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SQLiteStatement getDownloadByIndexStatement() {
|
private SQLiteStatement getDownloadByIndexStatement() {
|
||||||
if (null == mGetDownloadByIndex) {
|
if (null == mGetDownloadByIndex) {
|
||||||
mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
|
mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement(
|
||||||
"SELECT " + BaseColumns._ID + " FROM "
|
"SELECT " + BaseColumns._ID + " FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.INDEX + " = ?");
|
||||||
+ DownloadColumns.TABLE_NAME + " WHERE "
|
}
|
||||||
+ DownloadColumns.INDEX + " = ?");
|
return mGetDownloadByIndex;
|
||||||
}
|
}
|
||||||
return mGetDownloadByIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SQLiteStatement getUpdateCurrentBytesStatement() {
|
private SQLiteStatement getUpdateCurrentBytesStatement() {
|
||||||
if (null == mUpdateCurrentBytes) {
|
if (null == mUpdateCurrentBytes) {
|
||||||
mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
|
mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement(
|
||||||
"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES
|
"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + " = ?"
|
||||||
+ " = ?" +
|
+
|
||||||
" WHERE " + DownloadColumns.INDEX + " = ?");
|
" WHERE " + DownloadColumns.INDEX + " = ?");
|
||||||
}
|
}
|
||||||
return mUpdateCurrentBytes;
|
return mUpdateCurrentBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadsDB(Context paramContext) {
|
private DownloadsDB(Context paramContext) {
|
||||||
this.mHelper = new DownloadsContentDBHelper(paramContext);
|
this.mHelper = new DownloadsContentDBHelper(paramContext);
|
||||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||||
// Query for the version code, the row ID of the metadata (for future
|
// Query for the version code, the row ID of the metadata (for future
|
||||||
// updating) the status and the flags
|
// updating) the status and the flags
|
||||||
Cursor cur = sqldb.rawQuery("SELECT " +
|
Cursor cur = sqldb.rawQuery("SELECT " +
|
||||||
MetadataColumns.APKVERSION + "," +
|
MetadataColumns.APKVERSION + "," +
|
||||||
BaseColumns._ID + "," +
|
BaseColumns._ID + "," +
|
||||||
MetadataColumns.DOWNLOAD_STATUS + "," +
|
MetadataColumns.DOWNLOAD_STATUS + "," +
|
||||||
MetadataColumns.FLAGS +
|
MetadataColumns.FLAGS +
|
||||||
" FROM "
|
" FROM " + MetadataColumns.TABLE_NAME + " LIMIT 1",
|
||||||
+ MetadataColumns.TABLE_NAME + " LIMIT 1", null);
|
null);
|
||||||
if (null != cur && cur.moveToFirst()) {
|
if (null != cur && cur.moveToFirst()) {
|
||||||
mVersionCode = cur.getInt(0);
|
mVersionCode = cur.getInt(0);
|
||||||
mMetadataRowID = cur.getLong(1);
|
mMetadataRowID = cur.getLong(1);
|
||||||
mStatus = cur.getInt(2);
|
mStatus = cur.getInt(2);
|
||||||
mFlags = cur.getInt(3);
|
mFlags = cur.getInt(3);
|
||||||
cur.close();
|
cur.close();
|
||||||
}
|
}
|
||||||
mDownloadsDB = this;
|
mDownloadsDB = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DownloadInfo getDownloadInfoByFileName(String fileName) {
|
protected DownloadInfo getDownloadInfoByFileName(String fileName) {
|
||||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||||
Cursor itemcur = null;
|
Cursor itemcur = null;
|
||||||
try {
|
try {
|
||||||
itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
||||||
DownloadColumns.FILENAME + " = ?",
|
DownloadColumns.FILENAME + " = ?",
|
||||||
new String[] {
|
new String[] {
|
||||||
fileName
|
fileName },
|
||||||
}, null, null, null);
|
null, null, null);
|
||||||
if (null != itemcur && itemcur.moveToFirst()) {
|
if (null != itemcur && itemcur.moveToFirst()) {
|
||||||
return getDownloadInfoFromCursor(itemcur);
|
return getDownloadInfoFromCursor(itemcur);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (null != itemcur)
|
if (null != itemcur)
|
||||||
itemcur.close();
|
itemcur.close();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getIDForDownloadInfo(final DownloadInfo di) {
|
public long getIDForDownloadInfo(final DownloadInfo di) {
|
||||||
return getIDByIndex(di.mIndex);
|
return getIDByIndex(di.mIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getIDByIndex(int index) {
|
public long getIDByIndex(int index) {
|
||||||
SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
|
SQLiteStatement downloadByIndex = getDownloadByIndexStatement();
|
||||||
downloadByIndex.clearBindings();
|
downloadByIndex.clearBindings();
|
||||||
downloadByIndex.bindLong(1, index);
|
downloadByIndex.bindLong(1, index);
|
||||||
try {
|
try {
|
||||||
return downloadByIndex.simpleQueryForLong();
|
return downloadByIndex.simpleQueryForLong();
|
||||||
} catch (SQLiteDoneException e) {
|
} catch (SQLiteDoneException e) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateDownloadCurrentBytes(final DownloadInfo di) {
|
public void updateDownloadCurrentBytes(final DownloadInfo di) {
|
||||||
SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
|
SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement();
|
||||||
downloadCurrentBytes.clearBindings();
|
downloadCurrentBytes.clearBindings();
|
||||||
downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
|
downloadCurrentBytes.bindLong(1, di.mCurrentBytes);
|
||||||
downloadCurrentBytes.bindLong(2, di.mIndex);
|
downloadCurrentBytes.bindLong(2, di.mIndex);
|
||||||
downloadCurrentBytes.execute();
|
downloadCurrentBytes.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
this.mHelper.close();
|
this.mHelper.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
|
protected static class DownloadsContentDBHelper extends SQLiteOpenHelper {
|
||||||
DownloadsContentDBHelper(Context paramContext) {
|
DownloadsContentDBHelper(Context paramContext) {
|
||||||
super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
|
super(paramContext, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createTableQueryFromArray(String paramString,
|
private String createTableQueryFromArray(String paramString,
|
||||||
String[][] paramArrayOfString) {
|
String[][] paramArrayOfString) {
|
||||||
StringBuilder localStringBuilder = new StringBuilder();
|
StringBuilder localStringBuilder = new StringBuilder();
|
||||||
localStringBuilder.append("CREATE TABLE ");
|
localStringBuilder.append("CREATE TABLE ");
|
||||||
localStringBuilder.append(paramString);
|
localStringBuilder.append(paramString);
|
||||||
localStringBuilder.append(" (");
|
localStringBuilder.append(" (");
|
||||||
int i = paramArrayOfString.length;
|
int i = paramArrayOfString.length;
|
||||||
for (int j = 0;; j++) {
|
for (int j = 0;; j++) {
|
||||||
if (j >= i) {
|
if (j >= i) {
|
||||||
localStringBuilder
|
localStringBuilder
|
||||||
.setLength(localStringBuilder.length() - 1);
|
.setLength(localStringBuilder.length() - 1);
|
||||||
localStringBuilder.append(");");
|
localStringBuilder.append(");");
|
||||||
return localStringBuilder.toString();
|
return localStringBuilder.toString();
|
||||||
}
|
}
|
||||||
String[] arrayOfString = paramArrayOfString[j];
|
String[] arrayOfString = paramArrayOfString[j];
|
||||||
localStringBuilder.append(' ');
|
localStringBuilder.append(' ');
|
||||||
localStringBuilder.append(arrayOfString[0]);
|
localStringBuilder.append(arrayOfString[0]);
|
||||||
localStringBuilder.append(' ');
|
localStringBuilder.append(' ');
|
||||||
localStringBuilder.append(arrayOfString[1]);
|
localStringBuilder.append(arrayOfString[1]);
|
||||||
localStringBuilder.append(',');
|
localStringBuilder.append(',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These two arrays must match and have the same order. For every Schema
|
* These two arrays must match and have the same order. For every Schema
|
||||||
* there must be a corresponding table name.
|
* there must be a corresponding table name.
|
||||||
*/
|
*/
|
||||||
static final private String[][][] sSchemas = {
|
static final private String[][][] sSchemas = {
|
||||||
DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
|
DownloadColumns.SCHEMA, MetadataColumns.SCHEMA
|
||||||
};
|
};
|
||||||
|
|
||||||
static final private String[] sTables = {
|
static final private String[] sTables = {
|
||||||
DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
|
DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Goes through all of the tables in sTables and drops each table if it
|
* Goes through all of the tables in sTables and drops each table if it
|
||||||
* exists. Altered to no longer make use of reflection.
|
* exists. Altered to no longer make use of reflection.
|
||||||
*/
|
*/
|
||||||
private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
|
private void dropTables(SQLiteDatabase paramSQLiteDatabase) {
|
||||||
for (String table : sTables) {
|
for (String table : sTables) {
|
||||||
try {
|
try {
|
||||||
paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
|
paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table);
|
||||||
} catch (Exception localException) {
|
} catch (Exception localException) {
|
||||||
localException.printStackTrace();
|
localException.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Goes through all of the tables in sTables and creates a database with
|
* Goes through all of the tables in sTables and creates a database with
|
||||||
* the corresponding schema described in sSchemas. Altered to no longer
|
* the corresponding schema described in sSchemas. Altered to no longer
|
||||||
* make use of reflection.
|
* make use of reflection.
|
||||||
*/
|
*/
|
||||||
public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
|
public void onCreate(SQLiteDatabase paramSQLiteDatabase) {
|
||||||
int numSchemas = sSchemas.length;
|
int numSchemas = sSchemas.length;
|
||||||
for (int i = 0; i < numSchemas; i++) {
|
for (int i = 0; i < numSchemas; i++) {
|
||||||
try {
|
try {
|
||||||
String[][] schema = (String[][]) sSchemas[i];
|
String[][] schema = (String[][])sSchemas[i];
|
||||||
paramSQLiteDatabase.execSQL(createTableQueryFromArray(
|
paramSQLiteDatabase.execSQL(createTableQueryFromArray(
|
||||||
sTables[i], schema));
|
sTables[i], schema));
|
||||||
} catch (Exception localException) {
|
} catch (Exception localException) {
|
||||||
while (true)
|
while (true)
|
||||||
localException.printStackTrace();
|
localException.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
|
public void onUpgrade(SQLiteDatabase paramSQLiteDatabase,
|
||||||
int paramInt1, int paramInt2) {
|
int paramInt1, int paramInt2) {
|
||||||
Log.w(DownloadsContentDBHelper.class.getName(),
|
Log.w(DownloadsContentDBHelper.class.getName(),
|
||||||
"Upgrading database from version " + paramInt1 + " to "
|
"Upgrading database from version " + paramInt1 + " to " + paramInt2 + ", which will destroy all old data");
|
||||||
+ paramInt2 + ", which will destroy all old data");
|
dropTables(paramSQLiteDatabase);
|
||||||
dropTables(paramSQLiteDatabase);
|
onCreate(paramSQLiteDatabase);
|
||||||
onCreate(paramSQLiteDatabase);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class MetadataColumns implements BaseColumns {
|
public static class MetadataColumns implements BaseColumns {
|
||||||
public static final String APKVERSION = "APKVERSION";
|
public static final String APKVERSION = "APKVERSION";
|
||||||
public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
|
public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS";
|
||||||
public static final String FLAGS = "DOWNLOADFLAGS";
|
public static final String FLAGS = "DOWNLOADFLAGS";
|
||||||
|
|
||||||
public static final String[][] SCHEMA = {
|
public static final String[][] SCHEMA = {
|
||||||
{
|
{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
|
||||||
BaseColumns._ID, "INTEGER PRIMARY KEY"
|
{ APKVERSION, "INTEGER" }, { DOWNLOAD_STATUS, "INTEGER" },
|
||||||
},
|
{ FLAGS, "INTEGER" }
|
||||||
{
|
};
|
||||||
APKVERSION, "INTEGER"
|
public static final String TABLE_NAME = "MetadataColumns";
|
||||||
}, {
|
public static final String _ID = "MetadataColumns._id";
|
||||||
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 class DownloadColumns implements BaseColumns {
|
||||||
public static final String INDEX = "FILEIDX";
|
public static final String INDEX = "FILEIDX";
|
||||||
public static final String URI = "URI";
|
public static final String URI = "URI";
|
||||||
public static final String FILENAME = "FN";
|
public static final String FILENAME = "FN";
|
||||||
public static final String ETAG = "ETAG";
|
public static final String ETAG = "ETAG";
|
||||||
|
|
||||||
public static final String TOTALBYTES = "TOTALBYTES";
|
public static final String TOTALBYTES = "TOTALBYTES";
|
||||||
public static final String CURRENTBYTES = "CURRENTBYTES";
|
public static final String CURRENTBYTES = "CURRENTBYTES";
|
||||||
public static final String LASTMOD = "LASTMOD";
|
public static final String LASTMOD = "LASTMOD";
|
||||||
|
|
||||||
public static final String STATUS = "STATUS";
|
public static final String STATUS = "STATUS";
|
||||||
public static final String CONTROL = "CONTROL";
|
public static final String CONTROL = "CONTROL";
|
||||||
public static final String NUM_FAILED = "FAILCOUNT";
|
public static final String NUM_FAILED = "FAILCOUNT";
|
||||||
public static final String RETRY_AFTER = "RETRYAFTER";
|
public static final String RETRY_AFTER = "RETRYAFTER";
|
||||||
public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
|
public static final String REDIRECT_COUNT = "REDIRECTCOUNT";
|
||||||
|
|
||||||
public static final String[][] SCHEMA = {
|
public static final String[][] SCHEMA = {
|
||||||
{
|
{ BaseColumns._ID, "INTEGER PRIMARY KEY" },
|
||||||
BaseColumns._ID, "INTEGER PRIMARY KEY"
|
{ INDEX, "INTEGER UNIQUE" }, { URI, "TEXT" },
|
||||||
},
|
{ FILENAME, "TEXT UNIQUE" }, { ETAG, "TEXT" },
|
||||||
{
|
{ TOTALBYTES, "INTEGER" }, { CURRENTBYTES, "INTEGER" },
|
||||||
INDEX, "INTEGER UNIQUE"
|
{ LASTMOD, "INTEGER" }, { STATUS, "INTEGER" },
|
||||||
}, {
|
{ CONTROL, "INTEGER" }, { NUM_FAILED, "INTEGER" },
|
||||||
URI, "TEXT"
|
{ RETRY_AFTER, "INTEGER" }, { REDIRECT_COUNT, "INTEGER" }
|
||||||
},
|
};
|
||||||
{
|
public static final String TABLE_NAME = "DownloadColumns";
|
||||||
FILENAME, "TEXT UNIQUE"
|
public static final String _ID = "DownloadColumns._id";
|
||||||
}, {
|
}
|
||||||
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 = {
|
private static final String[] DC_PROJECTION = {
|
||||||
DownloadColumns.FILENAME,
|
DownloadColumns.FILENAME,
|
||||||
DownloadColumns.URI, DownloadColumns.ETAG,
|
DownloadColumns.URI, DownloadColumns.ETAG,
|
||||||
DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
|
DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES,
|
||||||
DownloadColumns.LASTMOD, DownloadColumns.STATUS,
|
DownloadColumns.LASTMOD, DownloadColumns.STATUS,
|
||||||
DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
|
DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED,
|
||||||
DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
|
DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT,
|
||||||
DownloadColumns.INDEX
|
DownloadColumns.INDEX
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final int FILENAME_IDX = 0;
|
private static final int FILENAME_IDX = 0;
|
||||||
private static final int URI_IDX = 1;
|
private static final int URI_IDX = 1;
|
||||||
private static final int ETAG_IDX = 2;
|
private static final int ETAG_IDX = 2;
|
||||||
private static final int TOTALBYTES_IDX = 3;
|
private static final int TOTALBYTES_IDX = 3;
|
||||||
private static final int CURRENTBYTES_IDX = 4;
|
private static final int CURRENTBYTES_IDX = 4;
|
||||||
private static final int LASTMOD_IDX = 5;
|
private static final int LASTMOD_IDX = 5;
|
||||||
private static final int STATUS_IDX = 6;
|
private static final int STATUS_IDX = 6;
|
||||||
private static final int CONTROL_IDX = 7;
|
private static final int CONTROL_IDX = 7;
|
||||||
private static final int NUM_FAILED_IDX = 8;
|
private static final int NUM_FAILED_IDX = 8;
|
||||||
private static final int RETRY_AFTER_IDX = 9;
|
private static final int RETRY_AFTER_IDX = 9;
|
||||||
private static final int REDIRECT_COUNT_IDX = 10;
|
private static final int REDIRECT_COUNT_IDX = 10;
|
||||||
private static final int INDEX_IDX = 11;
|
private static final int INDEX_IDX = 11;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function will add a new file to the database if it does not exist.
|
* This function will add a new file to the database if it does not exist.
|
||||||
*
|
*
|
||||||
* @param di DownloadInfo that we wish to store
|
* @param di DownloadInfo that we wish to store
|
||||||
* @return the row id of the record to be updated/inserted, or -1
|
* @return the row id of the record to be updated/inserted, or -1
|
||||||
*/
|
*/
|
||||||
public boolean updateDownload(DownloadInfo di) {
|
public boolean updateDownload(DownloadInfo di) {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
cv.put(DownloadColumns.INDEX, di.mIndex);
|
cv.put(DownloadColumns.INDEX, di.mIndex);
|
||||||
cv.put(DownloadColumns.FILENAME, di.mFileName);
|
cv.put(DownloadColumns.FILENAME, di.mFileName);
|
||||||
cv.put(DownloadColumns.URI, di.mUri);
|
cv.put(DownloadColumns.URI, di.mUri);
|
||||||
cv.put(DownloadColumns.ETAG, di.mETag);
|
cv.put(DownloadColumns.ETAG, di.mETag);
|
||||||
cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
|
cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes);
|
||||||
cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
|
cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes);
|
||||||
cv.put(DownloadColumns.LASTMOD, di.mLastMod);
|
cv.put(DownloadColumns.LASTMOD, di.mLastMod);
|
||||||
cv.put(DownloadColumns.STATUS, di.mStatus);
|
cv.put(DownloadColumns.STATUS, di.mStatus);
|
||||||
cv.put(DownloadColumns.CONTROL, di.mControl);
|
cv.put(DownloadColumns.CONTROL, di.mControl);
|
||||||
cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
|
cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed);
|
||||||
cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
|
cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter);
|
||||||
cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
|
cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount);
|
||||||
return updateDownload(di, cv);
|
return updateDownload(di, cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateDownload(DownloadInfo di, ContentValues cv) {
|
public boolean updateDownload(DownloadInfo di, ContentValues cv) {
|
||||||
long id = di == null ? -1 : getIDForDownloadInfo(di);
|
long id = di == null ? -1 : getIDForDownloadInfo(di);
|
||||||
try {
|
try {
|
||||||
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
|
if (1 != sqldb.update(DownloadColumns.TABLE_NAME,
|
||||||
cv, DownloadColumns._ID + " = " + id, null)) {
|
cv, DownloadColumns._ID + " = " + id, null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
|
return -1 != sqldb.insert(DownloadColumns.TABLE_NAME,
|
||||||
DownloadColumns.URI, cv);
|
DownloadColumns.URI, cv);
|
||||||
}
|
}
|
||||||
} catch (android.database.sqlite.SQLiteException ex) {
|
} catch (android.database.sqlite.SQLiteException ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLastCheckedVersionCode() {
|
public int getLastCheckedVersionCode() {
|
||||||
return mVersionCode;
|
return mVersionCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDownloadRequired() {
|
public boolean isDownloadRequired() {
|
||||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||||
Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM "
|
Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.STATUS + " <> 0", null);
|
||||||
+ DownloadColumns.TABLE_NAME + " WHERE "
|
try {
|
||||||
+ DownloadColumns.STATUS + " <> 0", null);
|
if (null != cur && cur.moveToFirst()) {
|
||||||
try {
|
return 0 == cur.getInt(0);
|
||||||
if (null != cur && cur.moveToFirst()) {
|
}
|
||||||
return 0 == cur.getInt(0);
|
} finally {
|
||||||
}
|
if (null != cur)
|
||||||
} finally {
|
cur.close();
|
||||||
if (null != cur)
|
}
|
||||||
cur.close();
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getFlags() {
|
public int getFlags() {
|
||||||
return mFlags;
|
return mFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateFlags(int flags) {
|
public boolean updateFlags(int flags) {
|
||||||
if (mFlags != flags) {
|
if (mFlags != flags) {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
cv.put(MetadataColumns.FLAGS, flags);
|
cv.put(MetadataColumns.FLAGS, flags);
|
||||||
if (updateMetadata(cv)) {
|
if (updateMetadata(cv)) {
|
||||||
mFlags = flags;
|
mFlags = flags;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public boolean updateStatus(int status) {
|
public boolean updateStatus(int status) {
|
||||||
if (mStatus != status) {
|
if (mStatus != status) {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
|
cv.put(MetadataColumns.DOWNLOAD_STATUS, status);
|
||||||
if (updateMetadata(cv)) {
|
if (updateMetadata(cv)) {
|
||||||
mStatus = status;
|
mStatus = status;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public boolean updateMetadata(ContentValues cv) {
|
public boolean updateMetadata(ContentValues cv) {
|
||||||
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getWritableDatabase();
|
||||||
if (-1 == this.mMetadataRowID) {
|
if (-1 == this.mMetadataRowID) {
|
||||||
long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
|
long newID = sqldb.insert(MetadataColumns.TABLE_NAME,
|
||||||
MetadataColumns.APKVERSION, cv);
|
MetadataColumns.APKVERSION, cv);
|
||||||
if (-1 == newID)
|
if (-1 == newID)
|
||||||
return false;
|
return false;
|
||||||
mMetadataRowID = newID;
|
mMetadataRowID = newID;
|
||||||
} else {
|
} else {
|
||||||
if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
|
if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv,
|
||||||
BaseColumns._ID + " = " + mMetadataRowID, null))
|
BaseColumns._ID + " = " + mMetadataRowID, null))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateMetadata(int apkVersion, int downloadStatus) {
|
public boolean updateMetadata(int apkVersion, int downloadStatus) {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
cv.put(MetadataColumns.APKVERSION, apkVersion);
|
cv.put(MetadataColumns.APKVERSION, apkVersion);
|
||||||
cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
|
cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus);
|
||||||
if (updateMetadata(cv)) {
|
if (updateMetadata(cv)) {
|
||||||
mVersionCode = apkVersion;
|
mVersionCode = apkVersion;
|
||||||
mStatus = downloadStatus;
|
mStatus = downloadStatus;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public boolean updateFromDb(DownloadInfo di) {
|
public boolean updateFromDb(DownloadInfo di) {
|
||||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||||
Cursor cur = null;
|
Cursor cur = null;
|
||||||
try {
|
try {
|
||||||
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION,
|
||||||
DownloadColumns.FILENAME + "= ?",
|
DownloadColumns.FILENAME + "= ?",
|
||||||
new String[] {
|
new String[] {
|
||||||
di.mFileName
|
di.mFileName },
|
||||||
}, null, null, null);
|
null, null, null);
|
||||||
if (null != cur && cur.moveToFirst()) {
|
if (null != cur && cur.moveToFirst()) {
|
||||||
setDownloadInfoFromCursor(di, cur);
|
setDownloadInfoFromCursor(di, cur);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
if (null != cur) {
|
if (null != cur) {
|
||||||
cur.close();
|
cur.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
|
public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) {
|
||||||
di.mUri = cur.getString(URI_IDX);
|
di.mUri = cur.getString(URI_IDX);
|
||||||
di.mETag = cur.getString(ETAG_IDX);
|
di.mETag = cur.getString(ETAG_IDX);
|
||||||
di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
|
di.mTotalBytes = cur.getLong(TOTALBYTES_IDX);
|
||||||
di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
|
di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX);
|
||||||
di.mLastMod = cur.getLong(LASTMOD_IDX);
|
di.mLastMod = cur.getLong(LASTMOD_IDX);
|
||||||
di.mStatus = cur.getInt(STATUS_IDX);
|
di.mStatus = cur.getInt(STATUS_IDX);
|
||||||
di.mControl = cur.getInt(CONTROL_IDX);
|
di.mControl = cur.getInt(CONTROL_IDX);
|
||||||
di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
|
di.mNumFailed = cur.getInt(NUM_FAILED_IDX);
|
||||||
di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
|
di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX);
|
||||||
di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
|
di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
|
public DownloadInfo getDownloadInfoFromCursor(Cursor cur) {
|
||||||
DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
|
DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX),
|
||||||
cur.getString(FILENAME_IDX), this.getClass().getPackage()
|
cur.getString(FILENAME_IDX), this.getClass().getPackage().getName());
|
||||||
.getName());
|
setDownloadInfoFromCursor(di, cur);
|
||||||
setDownloadInfoFromCursor(di, cur);
|
return di;
|
||||||
return di;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public DownloadInfo[] getDownloads() {
|
|
||||||
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
|
||||||
Cursor cur = null;
|
|
||||||
try {
|
|
||||||
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
|
|
||||||
null, null, null, null);
|
|
||||||
if (null != cur && cur.moveToFirst()) {
|
|
||||||
DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
|
|
||||||
int idx = 0;
|
|
||||||
do {
|
|
||||||
DownloadInfo di = getDownloadInfoFromCursor(cur);
|
|
||||||
retInfos[idx++] = di;
|
|
||||||
} while (cur.moveToNext());
|
|
||||||
return retInfos;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
if (null != cur) {
|
|
||||||
cur.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public DownloadInfo[] getDownloads() {
|
||||||
|
final SQLiteDatabase sqldb = mHelper.getReadableDatabase();
|
||||||
|
Cursor cur = null;
|
||||||
|
try {
|
||||||
|
cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null,
|
||||||
|
null, null, null, null);
|
||||||
|
if (null != cur && cur.moveToFirst()) {
|
||||||
|
DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()];
|
||||||
|
int idx = 0;
|
||||||
|
do {
|
||||||
|
DownloadInfo di = getDownloadInfoFromCursor(cur);
|
||||||
|
retInfos[idx++] = di;
|
||||||
|
} while (cur.moveToNext());
|
||||||
|
return retInfos;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (null != cur) {
|
||||||
|
cur.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public final class HttpDateTime {
|
public final class HttpDateTime {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT
|
* 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,
|
* 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
|
* 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
|
* (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.
|
* digit is zero. Mon can be the full name of the month.
|
||||||
*/
|
*/
|
||||||
private static final String HTTP_DATE_RFC_REGEXP =
|
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})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]"
|
||||||
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
|
+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])";
|
||||||
|
|
||||||
private static final String HTTP_DATE_ANSIC_REGEXP =
|
private static final String HTTP_DATE_ANSIC_REGEXP =
|
||||||
"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]"
|
"[ ]([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})";
|
+ "([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.
|
* The compiled version of the HTTP-date regular expressions.
|
||||||
*/
|
*/
|
||||||
private static final Pattern HTTP_DATE_RFC_PATTERN =
|
private static final Pattern HTTP_DATE_RFC_PATTERN =
|
||||||
Pattern.compile(HTTP_DATE_RFC_REGEXP);
|
Pattern.compile(HTTP_DATE_RFC_REGEXP);
|
||||||
private static final Pattern HTTP_DATE_ANSIC_PATTERN =
|
private static final Pattern HTTP_DATE_ANSIC_PATTERN =
|
||||||
Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
|
Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
|
||||||
|
|
||||||
private static class TimeOfDay {
|
private static class TimeOfDay {
|
||||||
TimeOfDay(int h, int m, int s) {
|
TimeOfDay(int h, int m, int s) {
|
||||||
this.hour = h;
|
this.hour = h;
|
||||||
this.minute = m;
|
this.minute = m;
|
||||||
this.second = s;
|
this.second = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
int hour;
|
int hour;
|
||||||
int minute;
|
int minute;
|
||||||
int second;
|
int second;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long parse(String timeString)
|
public static long parse(String timeString)
|
||||||
throws IllegalArgumentException {
|
throws IllegalArgumentException {
|
||||||
|
|
||||||
int date = 1;
|
int date = 1;
|
||||||
int month = Calendar.JANUARY;
|
int month = Calendar.JANUARY;
|
||||||
int year = 1970;
|
int year = 1970;
|
||||||
TimeOfDay timeOfDay;
|
TimeOfDay timeOfDay;
|
||||||
|
|
||||||
Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
|
Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
|
||||||
if (rfcMatcher.find()) {
|
if (rfcMatcher.find()) {
|
||||||
date = getDate(rfcMatcher.group(1));
|
date = getDate(rfcMatcher.group(1));
|
||||||
month = getMonth(rfcMatcher.group(2));
|
month = getMonth(rfcMatcher.group(2));
|
||||||
year = getYear(rfcMatcher.group(3));
|
year = getYear(rfcMatcher.group(3));
|
||||||
timeOfDay = getTime(rfcMatcher.group(4));
|
timeOfDay = getTime(rfcMatcher.group(4));
|
||||||
} else {
|
} else {
|
||||||
Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
|
Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
|
||||||
if (ansicMatcher.find()) {
|
if (ansicMatcher.find()) {
|
||||||
month = getMonth(ansicMatcher.group(1));
|
month = getMonth(ansicMatcher.group(1));
|
||||||
date = getDate(ansicMatcher.group(2));
|
date = getDate(ansicMatcher.group(2));
|
||||||
timeOfDay = getTime(ansicMatcher.group(3));
|
timeOfDay = getTime(ansicMatcher.group(3));
|
||||||
year = getYear(ansicMatcher.group(4));
|
year = getYear(ansicMatcher.group(4));
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Y2038 BUG!
|
// FIXME: Y2038 BUG!
|
||||||
if (year >= 2038) {
|
if (year >= 2038) {
|
||||||
year = 2038;
|
year = 2038;
|
||||||
month = Calendar.JANUARY;
|
month = Calendar.JANUARY;
|
||||||
date = 1;
|
date = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Time time = new Time(Time.TIMEZONE_UTC);
|
Time time = new Time(Time.TIMEZONE_UTC);
|
||||||
time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
|
time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
|
||||||
month, year);
|
month, year);
|
||||||
return time.toMillis(false /* use isDst */);
|
return time.toMillis(false /* use isDst */);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getDate(String dateString) {
|
private static int getDate(String dateString) {
|
||||||
if (dateString.length() == 2) {
|
if (dateString.length() == 2) {
|
||||||
return (dateString.charAt(0) - '0') * 10
|
return (dateString.charAt(0) - '0') * 10 + (dateString.charAt(1) - '0');
|
||||||
+ (dateString.charAt(1) - '0');
|
} else {
|
||||||
} else {
|
return (dateString.charAt(0) - '0');
|
||||||
return (dateString.charAt(0) - '0');
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 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
|
* + 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
|
* + 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
|
* = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9
|
||||||
*/
|
*/
|
||||||
private static int getMonth(String monthString) {
|
private static int getMonth(String monthString) {
|
||||||
int hash = Character.toLowerCase(monthString.charAt(0)) +
|
int hash = Character.toLowerCase(monthString.charAt(0)) +
|
||||||
Character.toLowerCase(monthString.charAt(1)) +
|
Character.toLowerCase(monthString.charAt(1)) +
|
||||||
Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
|
Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
|
||||||
switch (hash) {
|
switch (hash) {
|
||||||
case 22:
|
case 22:
|
||||||
return Calendar.JANUARY;
|
return Calendar.JANUARY;
|
||||||
case 10:
|
case 10:
|
||||||
return Calendar.FEBRUARY;
|
return Calendar.FEBRUARY;
|
||||||
case 29:
|
case 29:
|
||||||
return Calendar.MARCH;
|
return Calendar.MARCH;
|
||||||
case 32:
|
case 32:
|
||||||
return Calendar.APRIL;
|
return Calendar.APRIL;
|
||||||
case 36:
|
case 36:
|
||||||
return Calendar.MAY;
|
return Calendar.MAY;
|
||||||
case 42:
|
case 42:
|
||||||
return Calendar.JUNE;
|
return Calendar.JUNE;
|
||||||
case 40:
|
case 40:
|
||||||
return Calendar.JULY;
|
return Calendar.JULY;
|
||||||
case 26:
|
case 26:
|
||||||
return Calendar.AUGUST;
|
return Calendar.AUGUST;
|
||||||
case 37:
|
case 37:
|
||||||
return Calendar.SEPTEMBER;
|
return Calendar.SEPTEMBER;
|
||||||
case 35:
|
case 35:
|
||||||
return Calendar.OCTOBER;
|
return Calendar.OCTOBER;
|
||||||
case 48:
|
case 48:
|
||||||
return Calendar.NOVEMBER;
|
return Calendar.NOVEMBER;
|
||||||
case 9:
|
case 9:
|
||||||
return Calendar.DECEMBER;
|
return Calendar.DECEMBER;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getYear(String yearString) {
|
private static int getYear(String yearString) {
|
||||||
if (yearString.length() == 2) {
|
if (yearString.length() == 2) {
|
||||||
int year = (yearString.charAt(0) - '0') * 10
|
int year = (yearString.charAt(0) - '0') * 10 + (yearString.charAt(1) - '0');
|
||||||
+ (yearString.charAt(1) - '0');
|
if (year >= 70) {
|
||||||
if (year >= 70) {
|
return year + 1900;
|
||||||
return year + 1900;
|
} else {
|
||||||
} else {
|
return year + 2000;
|
||||||
return year + 2000;
|
}
|
||||||
}
|
} else if (yearString.length() == 3) {
|
||||||
} else if (yearString.length() == 3) {
|
// According to RFC 2822, three digit years should be added to 1900.
|
||||||
// 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');
|
||||||
int year = (yearString.charAt(0) - '0') * 100
|
return year + 1900;
|
||||||
+ (yearString.charAt(1) - '0') * 10
|
} else if (yearString.length() == 4) {
|
||||||
+ (yearString.charAt(2) - '0');
|
return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0');
|
||||||
return year + 1900;
|
} else {
|
||||||
} else if (yearString.length() == 4) {
|
return 1970;
|
||||||
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) {
|
private static TimeOfDay getTime(String timeString) {
|
||||||
// HH might be H
|
// HH might be H
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int hour = timeString.charAt(i++) - '0';
|
int hour = timeString.charAt(i++) - '0';
|
||||||
if (timeString.charAt(i) != ':')
|
if (timeString.charAt(i) != ':')
|
||||||
hour = hour * 10 + (timeString.charAt(i++) - '0');
|
hour = hour * 10 + (timeString.charAt(i++) - '0');
|
||||||
// Skip ':'
|
// Skip ':'
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
int minute = (timeString.charAt(i++) - '0') * 10
|
int minute = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0');
|
||||||
+ (timeString.charAt(i++) - '0');
|
// Skip ':'
|
||||||
// Skip ':'
|
i++;
|
||||||
i++;
|
|
||||||
|
|
||||||
int second = (timeString.charAt(i++) - '0') * 10
|
int second = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0');
|
||||||
+ (timeString.charAt(i++) - '0');
|
|
||||||
|
|
||||||
return new TimeOfDay(hour, minute, second);
|
return new TimeOfDay(hour, minute, second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.vending.expansion.downloader.impl;
|
|
||||||
|
|
||||||
import com.godot.game.R;
|
|
||||||
import com.google.android.vending.expansion.downloader.Helpers;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class V14CustomNotification implements DownloadNotification.ICustomNotification {
|
|
||||||
|
|
||||||
CharSequence mTitle;
|
|
||||||
CharSequence mTicker;
|
|
||||||
int mIcon;
|
|
||||||
long mTotalKB = -1;
|
|
||||||
long mCurrentKB = -1;
|
|
||||||
long mTimeRemaining;
|
|
||||||
PendingIntent mPendingIntent;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIcon(int icon) {
|
|
||||||
mIcon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(CharSequence title) {
|
|
||||||
mTitle = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTotalBytes(long totalBytes) {
|
|
||||||
mTotalKB = totalBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCurrentBytes(long currentBytes) {
|
|
||||||
mCurrentKB = currentBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setProgress(Notification.Builder builder) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Notification.Builder updateNotification(Context c) {
|
|
||||||
Notification.Builder builder = new Notification.Builder(c);
|
|
||||||
builder.setContentTitle(mTitle);
|
|
||||||
if (mTotalKB > 0 && -1 != mCurrentKB) {
|
|
||||||
builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false);
|
|
||||||
} else {
|
|
||||||
builder.setProgress(0, 0, true);
|
|
||||||
}
|
|
||||||
builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB));
|
|
||||||
builder.setContentInfo(c.getString(R.string.time_remaining_notification,
|
|
||||||
Helpers.getTimeRemaining(mTimeRemaining)));
|
|
||||||
if (mIcon != 0) {
|
|
||||||
builder.setSmallIcon(mIcon);
|
|
||||||
} else {
|
|
||||||
int iconResource = android.R.drawable.stat_sys_download;
|
|
||||||
builder.setSmallIcon(iconResource);
|
|
||||||
}
|
|
||||||
builder.setOngoing(true);
|
|
||||||
builder.setTicker(mTicker);
|
|
||||||
builder.setContentIntent(mPendingIntent);
|
|
||||||
builder.setOnlyAlertOnce(true);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPendingIntent(PendingIntent contentIntent) {
|
|
||||||
mPendingIntent = contentIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTicker(CharSequence ticker) {
|
|
||||||
mTicker = ticker;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTimeRemaining(long timeRemaining) {
|
|
||||||
mTimeRemaining = timeRemaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import com.google.android.vending.licensing.util.Base64;
|
||||||
|
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Obfuscator that uses AES to encrypt data.
|
||||||
|
*/
|
||||||
|
public class AESObfuscator implements Obfuscator {
|
||||||
|
private static final String UTF8 = "UTF-8";
|
||||||
|
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
|
||||||
|
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||||
|
private static final byte[] IV = { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
|
||||||
|
private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|";
|
||||||
|
|
||||||
|
private Cipher mEncryptor;
|
||||||
|
private Cipher mDecryptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param salt an array of random bytes to use for each (un)obfuscation
|
||||||
|
* @param applicationId application identifier, e.g. the package name
|
||||||
|
* @param deviceId device identifier. Use as many sources as possible to
|
||||||
|
* create this unique identifier.
|
||||||
|
*/
|
||||||
|
public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
|
||||||
|
try {
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
|
||||||
|
KeySpec keySpec =
|
||||||
|
new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
|
||||||
|
SecretKey tmp = factory.generateSecret(keySpec);
|
||||||
|
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
|
||||||
|
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||||
|
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
|
||||||
|
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||||
|
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
// This can't happen on a compatible Android device.
|
||||||
|
throw new RuntimeException("Invalid environment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String obfuscate(String original, String key) {
|
||||||
|
if (original == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Header is appended as an integrity check
|
||||||
|
return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Invalid environment", e);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new RuntimeException("Invalid environment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String unobfuscate(String obfuscated, String key) throws ValidationException {
|
||||||
|
if (obfuscated == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
|
||||||
|
// Check for presence of header. This serves as a final integrity check, for cases
|
||||||
|
// where the block size is correct during decryption.
|
||||||
|
int headerIndex = result.indexOf(header + key);
|
||||||
|
if (headerIndex != 0) {
|
||||||
|
throw new ValidationException("Header not found (invalid data or key)"
|
||||||
|
+ ":" +
|
||||||
|
obfuscated);
|
||||||
|
}
|
||||||
|
return result.substring(header.length() + key.length(), result.length());
|
||||||
|
} catch (Base64DecoderException e) {
|
||||||
|
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||||
|
} catch (IllegalBlockSizeException e) {
|
||||||
|
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||||
|
} catch (BadPaddingException e) {
|
||||||
|
throw new ValidationException(e.getMessage() + ":" + obfuscated);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Invalid environment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,413 @@
|
|||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.vending.licensing.util.URIQueryDecoder;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default policy. All policy decisions are based off of response data received
|
||||||
|
* from the licensing service. Specifically, the licensing server sends the
|
||||||
|
* following information: response validity period, error retry period,
|
||||||
|
* error retry count and a URL for restoring app access in unlicensed cases.
|
||||||
|
* <p>
|
||||||
|
* These values will vary based on the the way the application is configured in
|
||||||
|
* the Google Play publishing console, such as whether the application is
|
||||||
|
* marked as free or is within its refund period, as well as how often an
|
||||||
|
* application is checking with the licensing service.
|
||||||
|
* <p>
|
||||||
|
* Developers who need more fine grained control over their application's
|
||||||
|
* licensing policy should implement a custom Policy.
|
||||||
|
*/
|
||||||
|
public class APKExpansionPolicy implements Policy {
|
||||||
|
|
||||||
|
private static final String TAG = "APKExpansionPolicy";
|
||||||
|
private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy";
|
||||||
|
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
||||||
|
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
||||||
|
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
||||||
|
private static final String PREF_MAX_RETRIES = "maxRetries";
|
||||||
|
private static final String PREF_RETRY_COUNT = "retryCount";
|
||||||
|
private static final String PREF_LICENSING_URL = "licensingUrl";
|
||||||
|
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
||||||
|
private static final String DEFAULT_RETRY_UNTIL = "0";
|
||||||
|
private static final String DEFAULT_MAX_RETRIES = "0";
|
||||||
|
private static final String DEFAULT_RETRY_COUNT = "0";
|
||||||
|
|
||||||
|
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
|
private long mValidityTimestamp;
|
||||||
|
private long mRetryUntil;
|
||||||
|
private long mMaxRetries;
|
||||||
|
private long mRetryCount;
|
||||||
|
private long mLastResponseTime = 0;
|
||||||
|
private int mLastResponse;
|
||||||
|
private String mLicensingUrl;
|
||||||
|
private PreferenceObfuscator mPreferences;
|
||||||
|
private Vector<String> mExpansionURLs = new Vector<String>();
|
||||||
|
private Vector<String> mExpansionFileNames = new Vector<String>();
|
||||||
|
private Vector<Long> mExpansionFileSizes = new Vector<Long>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The design of the protocol supports n files. Currently the market can
|
||||||
|
* only deliver two files. To accommodate this, we have these two constants,
|
||||||
|
* but the order is the only relevant thing here.
|
||||||
|
*/
|
||||||
|
public static final int MAIN_FILE_URL_INDEX = 0;
|
||||||
|
public static final int PATCH_FILE_URL_INDEX = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context The context for the current application
|
||||||
|
* @param obfuscator An obfuscator to be used with preferences.
|
||||||
|
*/
|
||||||
|
public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
|
||||||
|
// Import old values
|
||||||
|
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
||||||
|
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
||||||
|
mLastResponse = Integer.parseInt(
|
||||||
|
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
||||||
|
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
||||||
|
DEFAULT_VALIDITY_TIMESTAMP));
|
||||||
|
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
||||||
|
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
||||||
|
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
||||||
|
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We call this to guarantee that we fetch a fresh policy from the server.
|
||||||
|
* This is to be used if the URL is invalid.
|
||||||
|
*/
|
||||||
|
public void resetPolicy() {
|
||||||
|
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
|
||||||
|
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||||
|
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||||
|
setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
|
||||||
|
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||||
|
mPreferences.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a new response from the license server.
|
||||||
|
* <p>
|
||||||
|
* This data will be used for computing future policy decisions. The
|
||||||
|
* following parameters are processed:
|
||||||
|
* <ul>
|
||||||
|
* <li>VT: the timestamp that the client should consider the response valid
|
||||||
|
* until
|
||||||
|
* <li>GT: the timestamp that the client should ignore retry errors until
|
||||||
|
* <li>GR: the number of retry errors that the client should ignore
|
||||||
|
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
|
||||||
|
* buy app on the Play Store)
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param response the result from validating the server response
|
||||||
|
* @param rawData the raw server response data
|
||||||
|
*/
|
||||||
|
public void processServerResponse(int response,
|
||||||
|
com.google.android.vending.licensing.ResponseData rawData) {
|
||||||
|
|
||||||
|
// Update retry counter
|
||||||
|
if (response != Policy.RETRY) {
|
||||||
|
setRetryCount(0);
|
||||||
|
} else {
|
||||||
|
setRetryCount(mRetryCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server policy data
|
||||||
|
Map<String, String> extras = decodeExtras(rawData);
|
||||||
|
if (response == Policy.LICENSED) {
|
||||||
|
mLastResponse = response;
|
||||||
|
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
|
||||||
|
setLicensingUrl(null);
|
||||||
|
setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
|
||||||
|
Set<String> keys = extras.keySet();
|
||||||
|
for (String key : keys) {
|
||||||
|
if (key.equals("VT")) {
|
||||||
|
setValidityTimestamp(extras.get(key));
|
||||||
|
} else if (key.equals("GT")) {
|
||||||
|
setRetryUntil(extras.get(key));
|
||||||
|
} else if (key.equals("GR")) {
|
||||||
|
setMaxRetries(extras.get(key));
|
||||||
|
} else if (key.startsWith("FILE_URL")) {
|
||||||
|
int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
|
||||||
|
setExpansionURL(index, extras.get(key));
|
||||||
|
} else if (key.startsWith("FILE_NAME")) {
|
||||||
|
int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
|
||||||
|
setExpansionFileName(index, extras.get(key));
|
||||||
|
} else if (key.startsWith("FILE_SIZE")) {
|
||||||
|
int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
|
||||||
|
setExpansionFileSize(index, Long.parseLong(extras.get(key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (response == Policy.NOT_LICENSED) {
|
||||||
|
// Clear out stale retry params
|
||||||
|
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||||
|
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||||
|
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||||
|
// Update the licensing URL
|
||||||
|
setLicensingUrl(extras.get("LU"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastResponse(response);
|
||||||
|
mPreferences.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last license response received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param l the response
|
||||||
|
*/
|
||||||
|
private void setLastResponse(int l) {
|
||||||
|
mLastResponseTime = System.currentTimeMillis();
|
||||||
|
mLastResponse = l;
|
||||||
|
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current retry count and add to preferences. You must manually
|
||||||
|
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param c the new retry count
|
||||||
|
*/
|
||||||
|
private void setRetryCount(long c) {
|
||||||
|
mRetryCount = c;
|
||||||
|
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRetryCount() {
|
||||||
|
return mRetryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last validity timestamp (VT) received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param validityTimestamp the VT string received
|
||||||
|
*/
|
||||||
|
private void setValidityTimestamp(String validityTimestamp) {
|
||||||
|
Long lValidityTimestamp;
|
||||||
|
try {
|
||||||
|
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parseable, expire in one minute.
|
||||||
|
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
||||||
|
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
||||||
|
validityTimestamp = Long.toString(lValidityTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
mValidityTimestamp = lValidityTimestamp;
|
||||||
|
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getValidityTimestamp() {
|
||||||
|
return mValidityTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the retry until timestamp (GT) received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param retryUntil the GT string received
|
||||||
|
*/
|
||||||
|
private void setRetryUntil(String retryUntil) {
|
||||||
|
Long lRetryUntil;
|
||||||
|
try {
|
||||||
|
lRetryUntil = Long.parseLong(retryUntil);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parseable, expire immediately
|
||||||
|
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
||||||
|
retryUntil = "0";
|
||||||
|
lRetryUntil = 0l;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRetryUntil = lRetryUntil;
|
||||||
|
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRetryUntil() {
|
||||||
|
return mRetryUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the max retries value (GR) as received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param maxRetries the GR string received
|
||||||
|
*/
|
||||||
|
private void setMaxRetries(String maxRetries) {
|
||||||
|
Long lMaxRetries;
|
||||||
|
try {
|
||||||
|
lMaxRetries = Long.parseLong(maxRetries);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parseable, expire immediately
|
||||||
|
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
||||||
|
maxRetries = "0";
|
||||||
|
lMaxRetries = 0l;
|
||||||
|
}
|
||||||
|
|
||||||
|
mMaxRetries = lMaxRetries;
|
||||||
|
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxRetries() {
|
||||||
|
return mMaxRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the licensing URL that displays a Play Store UI for the user to regain app access.
|
||||||
|
*
|
||||||
|
* @param url the LU string received
|
||||||
|
*/
|
||||||
|
private void setLicensingUrl(String url) {
|
||||||
|
mLicensingUrl = url;
|
||||||
|
mPreferences.putString(PREF_LICENSING_URL, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLicensingUrl() {
|
||||||
|
return mLicensingUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the count of expansion URLs. Since expansionURLs are not committed
|
||||||
|
* to preferences, this will return zero if there has been no LVL fetch
|
||||||
|
* in the current session.
|
||||||
|
*
|
||||||
|
* @return the number of expansion URLs. (0,1,2)
|
||||||
|
*/
|
||||||
|
public int getExpansionURLCount() {
|
||||||
|
return mExpansionURLs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the expansion URL. Since these URLs are not committed to
|
||||||
|
* preferences, this will always return null if there has not been an LVL
|
||||||
|
* fetch in the current session.
|
||||||
|
*
|
||||||
|
* @param index the index of the URL to fetch. This value will be either
|
||||||
|
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
||||||
|
*/
|
||||||
|
public String getExpansionURL(int index) {
|
||||||
|
if (index < mExpansionURLs.size()) {
|
||||||
|
return mExpansionURLs.elementAt(index);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the expansion URL. Expansion URL's are not committed to preferences,
|
||||||
|
* but are instead intended to be stored when the license response is
|
||||||
|
* processed by the front-end.
|
||||||
|
*
|
||||||
|
* @param index the index of the expansion URL. This value will be either
|
||||||
|
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
|
||||||
|
* @param URL the URL to set
|
||||||
|
*/
|
||||||
|
public void setExpansionURL(int index, String URL) {
|
||||||
|
if (index >= mExpansionURLs.size()) {
|
||||||
|
mExpansionURLs.setSize(index + 1);
|
||||||
|
}
|
||||||
|
mExpansionURLs.set(index, URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExpansionFileName(int index) {
|
||||||
|
if (index < mExpansionFileNames.size()) {
|
||||||
|
return mExpansionFileNames.elementAt(index);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpansionFileName(int index, String name) {
|
||||||
|
if (index >= mExpansionFileNames.size()) {
|
||||||
|
mExpansionFileNames.setSize(index + 1);
|
||||||
|
}
|
||||||
|
mExpansionFileNames.set(index, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpansionFileSize(int index) {
|
||||||
|
if (index < mExpansionFileSizes.size()) {
|
||||||
|
return mExpansionFileSizes.elementAt(index);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpansionFileSize(int index, long size) {
|
||||||
|
if (index >= mExpansionFileSizes.size()) {
|
||||||
|
mExpansionFileSizes.setSize(index + 1);
|
||||||
|
}
|
||||||
|
mExpansionFileSizes.set(index, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc} This implementation allows access if either:<br>
|
||||||
|
* <ol>
|
||||||
|
* <li>a LICENSED response was received within the validity period
|
||||||
|
* <li>a RETRY response was received in the last minute, and we are under
|
||||||
|
* the RETRY count or in the RETRY period.
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public boolean allowAccess() {
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
if (mLastResponse == Policy.LICENSED) {
|
||||||
|
// Check if the LICENSED response occurred within the validity
|
||||||
|
// timeout.
|
||||||
|
if (ts <= mValidityTimestamp) {
|
||||||
|
// Cached LICENSED response is still valid.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (mLastResponse == Policy.RETRY &&
|
||||||
|
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
||||||
|
// Only allow access if we are within the retry period or we haven't
|
||||||
|
// used up our
|
||||||
|
// max retries.
|
||||||
|
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> decodeExtras(
|
||||||
|
com.google.android.vending.licensing.ResponseData rawData) {
|
||||||
|
Map<String, String> results = new HashMap<String, String>();
|
||||||
|
if (rawData == null) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI rawExtras = new URI("?" + rawData.extra);
|
||||||
|
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
@ -37,11 +37,11 @@ package com.google.android.vending.licensing;
|
|||||||
*/
|
*/
|
||||||
public interface DeviceLimiter {
|
public interface DeviceLimiter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if this device is allowed to use the given user's license.
|
* Checks if this device is allowed to use the given user's license.
|
||||||
*
|
*
|
||||||
* @param userId the user whose license the server responded with
|
* @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
|
* @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
|
||||||
*/
|
*/
|
||||||
int isDeviceAllowed(String userId);
|
int isDeviceAllowed(String userId);
|
||||||
}
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
* Original file: aidl/ILicenseResultListener.aidl
|
||||||
|
*/
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
import java.lang.String;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Parcel;
|
||||||
|
public interface ILicenseResultListener extends android.os.IInterface {
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener {
|
||||||
|
private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener";
|
||||||
|
/** Construct the stub at attach it to the interface. */
|
||||||
|
public Stub() {
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an ILicenseResultListener interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) {
|
||||||
|
if ((obj == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) {
|
||||||
|
return ((com.google.android.vending.licensing.ILicenseResultListener)iin);
|
||||||
|
}
|
||||||
|
return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
|
||||||
|
switch (code) {
|
||||||
|
case INTERFACE_TRANSACTION: {
|
||||||
|
reply.writeString(DESCRIPTOR);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_verifyLicense: {
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
int _arg0;
|
||||||
|
_arg0 = data.readInt();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
java.lang.String _arg2;
|
||||||
|
_arg2 = data.readString();
|
||||||
|
this.verifyLicense(_arg0, _arg1, _arg2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener {
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote) {
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor() {
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException {
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeInt(responseCode);
|
||||||
|
_data.writeString(signedData);
|
||||||
|
_data.writeString(signature);
|
||||||
|
mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY);
|
||||||
|
} finally {
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
}
|
||||||
|
public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException;
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
* Original file: aidl/ILicensingService.aidl
|
||||||
|
*/
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
import java.lang.String;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Parcel;
|
||||||
|
public interface ILicensingService extends android.os.IInterface {
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService {
|
||||||
|
private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService";
|
||||||
|
/** Construct the stub at attach it to the interface. */
|
||||||
|
public Stub() {
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an ILicensingService interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) {
|
||||||
|
if ((obj == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicensingService))) {
|
||||||
|
return ((com.google.android.vending.licensing.ILicensingService)iin);
|
||||||
|
}
|
||||||
|
return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
|
||||||
|
switch (code) {
|
||||||
|
case INTERFACE_TRANSACTION: {
|
||||||
|
reply.writeString(DESCRIPTOR);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case TRANSACTION_checkLicense: {
|
||||||
|
data.enforceInterface(DESCRIPTOR);
|
||||||
|
long _arg0;
|
||||||
|
_arg0 = data.readLong();
|
||||||
|
java.lang.String _arg1;
|
||||||
|
_arg1 = data.readString();
|
||||||
|
com.google.android.vending.licensing.ILicenseResultListener _arg2;
|
||||||
|
_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder());
|
||||||
|
this.checkLicense(_arg0, _arg1, _arg2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
private static class Proxy implements com.google.android.vending.licensing.ILicensingService {
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote) {
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor() {
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException {
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeLong(nonce);
|
||||||
|
_data.writeString(packageName);
|
||||||
|
_data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
|
||||||
|
mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY);
|
||||||
|
} finally {
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
}
|
||||||
|
public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException;
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.provider.Settings.Secure;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.vending.licensing.ILicenseResultListener;
|
||||||
|
import com.google.android.vending.licensing.ILicensingService;
|
||||||
|
import com.google.android.vending.licensing.util.Base64;
|
||||||
|
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client library for Google Play license verifications.
|
||||||
|
* <p>
|
||||||
|
* The LicenseChecker is configured via a {@link Policy} which contains the logic to determine
|
||||||
|
* whether a user should have access to the application. For example, the Policy can define a
|
||||||
|
* threshold for allowable number of server or client failures before the library reports the user
|
||||||
|
* as not having access.
|
||||||
|
* <p>
|
||||||
|
* Must also provide the Base64-encoded RSA public key associated with your developer account. The
|
||||||
|
* public key is obtainable from the publisher site.
|
||||||
|
*/
|
||||||
|
public class LicenseChecker implements ServiceConnection {
|
||||||
|
private static final String TAG = "LicenseChecker";
|
||||||
|
|
||||||
|
private static final String KEY_FACTORY_ALGORITHM = "RSA";
|
||||||
|
|
||||||
|
// Timeout value (in milliseconds) for calls to service.
|
||||||
|
private static final int TIMEOUT_MS = 10 * 1000;
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
private static final boolean DEBUG_LICENSE_ERROR = false;
|
||||||
|
|
||||||
|
private ILicensingService mService;
|
||||||
|
|
||||||
|
private PublicKey mPublicKey;
|
||||||
|
private final Context mContext;
|
||||||
|
private final Policy mPolicy;
|
||||||
|
/**
|
||||||
|
* A handler for running tasks on a background thread. We don't want license processing to block
|
||||||
|
* the UI thread.
|
||||||
|
*/
|
||||||
|
private Handler mHandler;
|
||||||
|
private final String mPackageName;
|
||||||
|
private final String mVersionCode;
|
||||||
|
private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
|
||||||
|
private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context a Context
|
||||||
|
* @param policy implementation of Policy
|
||||||
|
* @param encodedPublicKey Base64-encoded RSA public key
|
||||||
|
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
||||||
|
*/
|
||||||
|
public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
|
||||||
|
mContext = context;
|
||||||
|
mPolicy = policy;
|
||||||
|
mPublicKey = generatePublicKey(encodedPublicKey);
|
||||||
|
mPackageName = mContext.getPackageName();
|
||||||
|
mVersionCode = getVersionCode(context, mPackageName);
|
||||||
|
HandlerThread handlerThread = new HandlerThread("background thread");
|
||||||
|
handlerThread.start();
|
||||||
|
mHandler = new Handler(handlerThread.getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PublicKey instance from a string containing the Base64-encoded public key.
|
||||||
|
*
|
||||||
|
* @param encodedPublicKey Base64-encoded public key
|
||||||
|
* @throws IllegalArgumentException if encodedPublicKey is invalid
|
||||||
|
*/
|
||||||
|
private static PublicKey generatePublicKey(String encodedPublicKey) {
|
||||||
|
try {
|
||||||
|
byte[] decodedKey = Base64.decode(encodedPublicKey);
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
||||||
|
|
||||||
|
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// This won't happen in an Android-compatible environment.
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (Base64DecoderException e) {
|
||||||
|
Log.e(TAG, "Could not decode from Base64.");
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
Log.e(TAG, "Invalid key specification.");
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user should have access to the app. Binds the service if necessary.
|
||||||
|
* <p>
|
||||||
|
* NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we
|
||||||
|
* recommend obfuscating the string that is passed into bindService using another method of your
|
||||||
|
* own devising.
|
||||||
|
* <p>
|
||||||
|
* source string: "com.android.vending.licensing.ILicensingService"
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
public synchronized void checkAccess(LicenseCheckerCallback callback) {
|
||||||
|
// If we have a valid recent LICENSED response, we can skip asking
|
||||||
|
// Market.
|
||||||
|
if (mPolicy.allowAccess()) {
|
||||||
|
Log.i(TAG, "Using cached license response");
|
||||||
|
callback.allow(Policy.LICENSED);
|
||||||
|
} else {
|
||||||
|
LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
|
||||||
|
callback, generateNonce(), mPackageName, mVersionCode);
|
||||||
|
|
||||||
|
if (mService == null) {
|
||||||
|
Log.i(TAG, "Binding to licensing service.");
|
||||||
|
try {
|
||||||
|
boolean bindResult = mContext
|
||||||
|
.bindService(
|
||||||
|
new Intent(
|
||||||
|
new String(
|
||||||
|
// Base64 encoded -
|
||||||
|
// com.android.vending.licensing.ILicensingService
|
||||||
|
// Consider encoding this in another way in your
|
||||||
|
// code to improve security
|
||||||
|
Base64.decode(
|
||||||
|
"Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
|
||||||
|
// As of Android 5.0, implicit
|
||||||
|
// Service Intents are no longer
|
||||||
|
// allowed because it's not
|
||||||
|
// possible for the user to
|
||||||
|
// participate in disambiguating
|
||||||
|
// them. This does mean we break
|
||||||
|
// compatibility with Android
|
||||||
|
// Cupcake devices with this
|
||||||
|
// release, since setPackage was
|
||||||
|
// added in Donut.
|
||||||
|
.setPackage(
|
||||||
|
new String(
|
||||||
|
// Base64
|
||||||
|
// encoded -
|
||||||
|
// com.android.vending
|
||||||
|
Base64.decode(
|
||||||
|
"Y29tLmFuZHJvaWQudmVuZGluZw=="))),
|
||||||
|
this, // ServiceConnection.
|
||||||
|
Context.BIND_AUTO_CREATE);
|
||||||
|
if (bindResult) {
|
||||||
|
mPendingChecks.offer(validator);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Could not bind to service.");
|
||||||
|
handleServiceConnectionError(validator);
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
|
||||||
|
} catch (Base64DecoderException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mPendingChecks.offer(validator);
|
||||||
|
runChecks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the last deep link licensing URL returned from the server, which redirects users to a
|
||||||
|
* page which enables them to gain access to the app. If no such URL is returned by the server, it
|
||||||
|
* will go to the details page of the app in the Play Store.
|
||||||
|
*/
|
||||||
|
public void followLastLicensingUrl(Context context) {
|
||||||
|
String licensingUrl = mPolicy.getLicensingUrl();
|
||||||
|
if (licensingUrl == null) {
|
||||||
|
licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
|
||||||
|
}
|
||||||
|
Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
|
||||||
|
context.startActivity(marketIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runChecks() {
|
||||||
|
LicenseValidator validator;
|
||||||
|
while ((validator = mPendingChecks.poll()) != null) {
|
||||||
|
try {
|
||||||
|
Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
|
||||||
|
mService.checkLicense(
|
||||||
|
validator.getNonce(), validator.getPackageName(),
|
||||||
|
new ResultListener(validator));
|
||||||
|
mChecksInProgress.add(validator);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "RemoteException in checkLicense call.", e);
|
||||||
|
handleServiceConnectionError(validator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void finishCheck(LicenseValidator validator) {
|
||||||
|
mChecksInProgress.remove(validator);
|
||||||
|
if (mChecksInProgress.isEmpty()) {
|
||||||
|
cleanupService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ResultListener extends ILicenseResultListener.Stub {
|
||||||
|
private final LicenseValidator mValidator;
|
||||||
|
private Runnable mOnTimeout;
|
||||||
|
|
||||||
|
public ResultListener(LicenseValidator validator) {
|
||||||
|
mValidator = validator;
|
||||||
|
mOnTimeout = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
Log.i(TAG, "Check timed out.");
|
||||||
|
handleServiceConnectionError(mValidator);
|
||||||
|
finishCheck(mValidator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
startTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
||||||
|
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
||||||
|
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
||||||
|
|
||||||
|
// Runs in IPC thread pool. Post it to the Handler, so we can guarantee
|
||||||
|
// either this or the timeout runs.
|
||||||
|
public void verifyLicense(final int responseCode, final String signedData,
|
||||||
|
final String signature) {
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
Log.i(TAG, "Received response.");
|
||||||
|
// Make sure it hasn't already timed out.
|
||||||
|
if (mChecksInProgress.contains(mValidator)) {
|
||||||
|
clearTimeout();
|
||||||
|
mValidator.verify(mPublicKey, responseCode, signedData, signature);
|
||||||
|
finishCheck(mValidator);
|
||||||
|
}
|
||||||
|
if (DEBUG_LICENSE_ERROR) {
|
||||||
|
boolean logResponse;
|
||||||
|
String stringError = null;
|
||||||
|
switch (responseCode) {
|
||||||
|
case ERROR_CONTACTING_SERVER:
|
||||||
|
logResponse = true;
|
||||||
|
stringError = "ERROR_CONTACTING_SERVER";
|
||||||
|
break;
|
||||||
|
case ERROR_INVALID_PACKAGE_NAME:
|
||||||
|
logResponse = true;
|
||||||
|
stringError = "ERROR_INVALID_PACKAGE_NAME";
|
||||||
|
break;
|
||||||
|
case ERROR_NON_MATCHING_UID:
|
||||||
|
logResponse = true;
|
||||||
|
stringError = "ERROR_NON_MATCHING_UID";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logResponse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logResponse) {
|
||||||
|
String android_id = Secure.ANDROID_ID;
|
||||||
|
Date date = new Date();
|
||||||
|
Log.d(TAG, "Server Failure: " + stringError);
|
||||||
|
Log.d(TAG, "Android ID: " + android_id);
|
||||||
|
Log.d(TAG, "Time: " + date.toGMTString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTimeout() {
|
||||||
|
Log.i(TAG, "Start monitoring timeout.");
|
||||||
|
mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearTimeout() {
|
||||||
|
Log.i(TAG, "Clearing timeout.");
|
||||||
|
mHandler.removeCallbacks(mOnTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
mService = ILicensingService.Stub.asInterface(service);
|
||||||
|
runChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onServiceDisconnected(ComponentName name) {
|
||||||
|
// Called when the connection with the service has been
|
||||||
|
// unexpectedly disconnected. That is, Market crashed.
|
||||||
|
// If there are any checks in progress, the timeouts will handle them.
|
||||||
|
Log.w(TAG, "Service unexpectedly disconnected.");
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates policy response for service connection errors, as a result of disconnections or
|
||||||
|
* timeouts.
|
||||||
|
*/
|
||||||
|
private synchronized void handleServiceConnectionError(LicenseValidator validator) {
|
||||||
|
mPolicy.processServerResponse(Policy.RETRY, null);
|
||||||
|
|
||||||
|
if (mPolicy.allowAccess()) {
|
||||||
|
validator.getCallback().allow(Policy.RETRY);
|
||||||
|
} else {
|
||||||
|
validator.getCallback().dontAllow(Policy.RETRY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unbinds service if necessary and removes reference to it. */
|
||||||
|
private void cleanupService() {
|
||||||
|
if (mService != null) {
|
||||||
|
try {
|
||||||
|
mContext.unbindService(this);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// Somehow we've already been unbound. This is a non-fatal
|
||||||
|
// error.
|
||||||
|
Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
|
||||||
|
}
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform the library that the context is about to be destroyed, so that any open connections
|
||||||
|
* can be cleaned up.
|
||||||
|
* <p>
|
||||||
|
* Failure to call this method can result in a crash under certain circumstances, such as during
|
||||||
|
* screen rotation if an Activity requests the license check or when the user exits the
|
||||||
|
* application.
|
||||||
|
*/
|
||||||
|
public synchronized void onDestroy() {
|
||||||
|
cleanupService();
|
||||||
|
mHandler.getLooper().quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generates a nonce (number used once). */
|
||||||
|
private int generateNonce() {
|
||||||
|
return RANDOM.nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get version code for the application package name.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* @param packageName application package name
|
||||||
|
* @return the version code or empty string if package not found
|
||||||
|
*/
|
||||||
|
private static String getVersionCode(Context context, String packageName) {
|
||||||
|
try {
|
||||||
|
return String.valueOf(
|
||||||
|
context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
|
||||||
|
} catch (NameNotFoundException e) {
|
||||||
|
Log.e(TAG, "Package not found. could not get version code.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,34 +34,34 @@ package com.google.android.vending.licensing;
|
|||||||
*/
|
*/
|
||||||
public interface LicenseCheckerCallback {
|
public interface LicenseCheckerCallback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow use. App should proceed as normal.
|
* Allow use. App should proceed as normal.
|
||||||
*
|
*
|
||||||
* @param reason Policy.LICENSED or Policy.RETRY typically. (although in
|
* @param reason Policy.LICENSED or Policy.RETRY typically. (although in
|
||||||
* theory the policy can return Policy.NOT_LICENSED here as well)
|
* 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.
|
* Don't allow use. App should inform user and take appropriate action.
|
||||||
*
|
*
|
||||||
* @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
|
* @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
|
||||||
* the policy can return Policy.LICENSED here as well ---
|
* the policy can return Policy.LICENSED here as well ---
|
||||||
* perhaps the call to the LVL took too long, for example)
|
* perhaps the call to the LVL took too long, for example)
|
||||||
*/
|
*/
|
||||||
public void dontAllow(int reason);
|
public void dontAllow(int reason);
|
||||||
|
|
||||||
/** Application error codes. */
|
/** Application error codes. */
|
||||||
public static final int ERROR_INVALID_PACKAGE_NAME = 1;
|
public static final int ERROR_INVALID_PACKAGE_NAME = 1;
|
||||||
public static final int ERROR_NON_MATCHING_UID = 2;
|
public static final int ERROR_NON_MATCHING_UID = 2;
|
||||||
public static final int ERROR_NOT_MARKET_MANAGED = 3;
|
public static final int ERROR_NOT_MARKET_MANAGED = 3;
|
||||||
public static final int ERROR_CHECK_IN_PROGRESS = 4;
|
public static final int ERROR_CHECK_IN_PROGRESS = 4;
|
||||||
public static final int ERROR_INVALID_PUBLIC_KEY = 5;
|
public static final int ERROR_INVALID_PUBLIC_KEY = 5;
|
||||||
public static final int ERROR_MISSING_PERMISSION = 6;
|
public static final int ERROR_MISSING_PERMISSION = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error in application code. Caller did not call or set up license checker
|
* Error in application code. Caller did not call or set up license checker
|
||||||
* correctly. Should be considered fatal.
|
* correctly. Should be considered fatal.
|
||||||
*/
|
*/
|
||||||
public void applicationError(int errorCode);
|
public void applicationError(int errorCode);
|
||||||
}
|
}
|
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import com.google.android.vending.licensing.util.Base64;
|
||||||
|
import com.google.android.vending.licensing.util.Base64DecoderException;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains data related to a licensing request and methods to verify
|
||||||
|
* and process the response.
|
||||||
|
*/
|
||||||
|
class LicenseValidator {
|
||||||
|
private static final String TAG = "LicenseValidator";
|
||||||
|
|
||||||
|
// Server response codes.
|
||||||
|
private static final int LICENSED = 0x0;
|
||||||
|
private static final int NOT_LICENSED = 0x1;
|
||||||
|
private static final int LICENSED_OLD_KEY = 0x2;
|
||||||
|
private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
|
||||||
|
private static final int ERROR_SERVER_FAILURE = 0x4;
|
||||||
|
private static final int ERROR_OVER_QUOTA = 0x5;
|
||||||
|
|
||||||
|
private static final int ERROR_CONTACTING_SERVER = 0x101;
|
||||||
|
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
|
||||||
|
private static final int ERROR_NON_MATCHING_UID = 0x103;
|
||||||
|
|
||||||
|
private final Policy mPolicy;
|
||||||
|
private final LicenseCheckerCallback mCallback;
|
||||||
|
private final int mNonce;
|
||||||
|
private final String mPackageName;
|
||||||
|
private final String mVersionCode;
|
||||||
|
private final DeviceLimiter mDeviceLimiter;
|
||||||
|
|
||||||
|
LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
|
||||||
|
int nonce, String packageName, String versionCode) {
|
||||||
|
mPolicy = policy;
|
||||||
|
mDeviceLimiter = deviceLimiter;
|
||||||
|
mCallback = callback;
|
||||||
|
mNonce = nonce;
|
||||||
|
mPackageName = packageName;
|
||||||
|
mVersionCode = versionCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseCheckerCallback getCallback() {
|
||||||
|
return mCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNonce() {
|
||||||
|
return mNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPackageName() {
|
||||||
|
return mPackageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the response from server and calls appropriate callback method.
|
||||||
|
*
|
||||||
|
* @param publicKey public key associated with the developer account
|
||||||
|
* @param responseCode server response code
|
||||||
|
* @param signedData signed data from server
|
||||||
|
* @param signature server signature
|
||||||
|
*/
|
||||||
|
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
|
||||||
|
String userId = null;
|
||||||
|
// Skip signature check for unsuccessful requests
|
||||||
|
ResponseData data = null;
|
||||||
|
if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
|
||||||
|
responseCode == LICENSED_OLD_KEY) {
|
||||||
|
// Verify signature.
|
||||||
|
try {
|
||||||
|
if (TextUtils.isEmpty(signedData)) {
|
||||||
|
Log.e(TAG, "Signature verification failed: signedData is empty. "
|
||||||
|
+
|
||||||
|
"(Device not signed-in to any Google accounts?)");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
sig.update(signedData.getBytes());
|
||||||
|
|
||||||
|
if (!sig.verify(Base64.decode(signature))) {
|
||||||
|
Log.e(TAG, "Signature verification failed.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// This can't happen on an Android compatible device.
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
|
||||||
|
return;
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (Base64DecoderException e) {
|
||||||
|
Log.e(TAG, "Could not Base64-decode signature.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate response.
|
||||||
|
try {
|
||||||
|
data = ResponseData.parse(signedData);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.e(TAG, "Could not parse response.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.responseCode != responseCode) {
|
||||||
|
Log.e(TAG, "Response codes don't match.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.nonce != mNonce) {
|
||||||
|
Log.e(TAG, "Nonce doesn't match.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.packageName.equals(mPackageName)) {
|
||||||
|
Log.e(TAG, "Package name doesn't match.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.versionCode.equals(mVersionCode)) {
|
||||||
|
Log.e(TAG, "Version codes don't match.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application-specific user identifier.
|
||||||
|
userId = data.userId;
|
||||||
|
if (TextUtils.isEmpty(userId)) {
|
||||||
|
Log.e(TAG, "User identifier is empty.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (responseCode) {
|
||||||
|
case LICENSED:
|
||||||
|
case LICENSED_OLD_KEY:
|
||||||
|
int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
|
||||||
|
handleResponse(limiterResponse, data);
|
||||||
|
break;
|
||||||
|
case NOT_LICENSED:
|
||||||
|
handleResponse(Policy.NOT_LICENSED, data);
|
||||||
|
break;
|
||||||
|
case ERROR_CONTACTING_SERVER:
|
||||||
|
Log.w(TAG, "Error contacting licensing server.");
|
||||||
|
handleResponse(Policy.RETRY, data);
|
||||||
|
break;
|
||||||
|
case ERROR_SERVER_FAILURE:
|
||||||
|
Log.w(TAG, "An error has occurred on the licensing server.");
|
||||||
|
handleResponse(Policy.RETRY, data);
|
||||||
|
break;
|
||||||
|
case ERROR_OVER_QUOTA:
|
||||||
|
Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
|
||||||
|
handleResponse(Policy.RETRY, data);
|
||||||
|
break;
|
||||||
|
case ERROR_INVALID_PACKAGE_NAME:
|
||||||
|
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
|
||||||
|
break;
|
||||||
|
case ERROR_NON_MATCHING_UID:
|
||||||
|
handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
|
||||||
|
break;
|
||||||
|
case ERROR_NOT_MARKET_MANAGED:
|
||||||
|
handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "Unknown response code for license check.");
|
||||||
|
handleInvalidResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confers with policy and calls appropriate callback method.
|
||||||
|
*
|
||||||
|
* @param response
|
||||||
|
* @param rawData
|
||||||
|
*/
|
||||||
|
private void handleResponse(int response, ResponseData rawData) {
|
||||||
|
// Update policy data and increment retry counter (if needed)
|
||||||
|
mPolicy.processServerResponse(response, rawData);
|
||||||
|
|
||||||
|
// Given everything we know, including cached data, ask the policy if we should grant
|
||||||
|
// access.
|
||||||
|
if (mPolicy.allowAccess()) {
|
||||||
|
mCallback.allow(response);
|
||||||
|
} else {
|
||||||
|
mCallback.dontAllow(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleApplicationError(int code) {
|
||||||
|
mCallback.applicationError(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleInvalidResponse() {
|
||||||
|
mCallback.dontAllow(Policy.NOT_LICENSED);
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ package com.google.android.vending.licensing;
|
|||||||
*/
|
*/
|
||||||
public class NullDeviceLimiter implements DeviceLimiter {
|
public class NullDeviceLimiter implements DeviceLimiter {
|
||||||
|
|
||||||
public int isDeviceAllowed(String userId) {
|
public int isDeviceAllowed(String userId) {
|
||||||
return Policy.LICENSED;
|
return Policy.LICENSED;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,29 +20,29 @@ package com.google.android.vending.licensing;
|
|||||||
* Interface used as part of a {@link Policy} to allow application authors to obfuscate
|
* Interface used as part of a {@link Policy} to allow application authors to obfuscate
|
||||||
* licensing data that will be stored into a SharedPreferences file.
|
* licensing data that will be stored into a SharedPreferences file.
|
||||||
* <p>
|
* <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
|
* 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
|
* should use device-specific information as a key in the obfuscation algorithm to prevent
|
||||||
* obfuscated preferences from being shared among devices.
|
* obfuscated preferences from being shared among devices.
|
||||||
*/
|
*/
|
||||||
public interface Obfuscator {
|
public interface Obfuscator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obfuscate a string that is being stored into shared preferences.
|
* Obfuscate a string that is being stored into shared preferences.
|
||||||
*
|
*
|
||||||
* @param original The data that is to be obfuscated.
|
* @param original The data that is to be obfuscated.
|
||||||
* @param key The key for 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.
|
* @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.
|
* Undo the transformation applied to data by the obfuscate() method.
|
||||||
*
|
*
|
||||||
* @param original The data that is to be obfuscated.
|
* @param obfuscated The data that is to be un-obfuscated.
|
||||||
* @param key The key for the data that is to be obfuscated.
|
* @param key The key for the data that is to be un-obfuscated.
|
||||||
* @return A transformed version of the original data.
|
* @return The original data transformed by the obfuscate() method.
|
||||||
* @throws ValidationException Optionally thrown if a data integrity check fails.
|
* @throws ValidationException Optionally thrown if a data integrity check fails.
|
||||||
*/
|
*/
|
||||||
String unobfuscate(String obfuscated, String key) throws ValidationException;
|
String unobfuscate(String obfuscated, String key) throws ValidationException;
|
||||||
}
|
}
|
@ -22,27 +22,27 @@ package com.google.android.vending.licensing;
|
|||||||
*/
|
*/
|
||||||
public interface Policy {
|
public interface Policy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change these values to make it more difficult for tools to automatically
|
* Change these values to make it more difficult for tools to automatically
|
||||||
* strip LVL protection from your APK.
|
* strip LVL protection from your APK.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LICENSED means that the server returned back a valid license response
|
* 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
|
* NOT_LICENSED means that the server returned back a valid license response
|
||||||
* that indicated that the user definitively is not licensed
|
* 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 ---
|
* RETRY means that the license response was unable to be determined ---
|
||||||
* perhaps as a result of faulty networking
|
* 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
|
* Provide results from contact with the license server. Retry counts are
|
||||||
* incremented if the current value of response is RETRY. Results will be
|
* incremented if the current value of response is RETRY. Results will be
|
||||||
* used for any future policy decisions.
|
* used for any future policy decisions.
|
||||||
@ -50,10 +50,16 @@ public interface Policy {
|
|||||||
* @param response the result from validating the server response
|
* @param response the result from validating the server response
|
||||||
* @param rawData the raw server response data, can be null for RETRY
|
* @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.
|
* Check if the user should be allowed access to the application.
|
||||||
*/
|
*/
|
||||||
boolean allowAccess();
|
boolean allowAccess();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g.
|
||||||
|
* buy app on the Play Store).
|
||||||
|
*/
|
||||||
|
String getLicensingUrl();
|
||||||
}
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An wrapper for SharedPreferences that transparently performs data obfuscation.
|
||||||
|
*/
|
||||||
|
public class PreferenceObfuscator {
|
||||||
|
|
||||||
|
private static final String TAG = "PreferenceObfuscator";
|
||||||
|
|
||||||
|
private final SharedPreferences mPreferences;
|
||||||
|
private final Obfuscator mObfuscator;
|
||||||
|
private SharedPreferences.Editor mEditor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param sp A SharedPreferences instance provided by the system.
|
||||||
|
* @param o The Obfuscator to use when reading or writing data.
|
||||||
|
*/
|
||||||
|
public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
|
||||||
|
mPreferences = sp;
|
||||||
|
mObfuscator = o;
|
||||||
|
mEditor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putString(String key, String value) {
|
||||||
|
if (mEditor == null) {
|
||||||
|
mEditor = mPreferences.edit();
|
||||||
|
mEditor.apply();
|
||||||
|
}
|
||||||
|
String obfuscatedValue = mObfuscator.obfuscate(value, key);
|
||||||
|
mEditor.putString(key, obfuscatedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(String key, String defValue) {
|
||||||
|
String result;
|
||||||
|
String value = mPreferences.getString(key, null);
|
||||||
|
if (value != null) {
|
||||||
|
try {
|
||||||
|
result = mObfuscator.unobfuscate(value, key);
|
||||||
|
} catch (ValidationException e) {
|
||||||
|
// Unable to unobfuscate, data corrupt or tampered
|
||||||
|
Log.w(TAG, "Validation error while reading preference: " + key);
|
||||||
|
result = defValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Preference not found
|
||||||
|
result = defValue;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void commit() {
|
||||||
|
if (mEditor != null) {
|
||||||
|
mEditor.commit();
|
||||||
|
mEditor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResponseData from licensing server.
|
||||||
|
*/
|
||||||
|
public class ResponseData {
|
||||||
|
|
||||||
|
public int responseCode;
|
||||||
|
public int nonce;
|
||||||
|
public String packageName;
|
||||||
|
public String versionCode;
|
||||||
|
public String userId;
|
||||||
|
public long timestamp;
|
||||||
|
/** Response-specific data. */
|
||||||
|
public String extra;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses response string into ResponseData.
|
||||||
|
*
|
||||||
|
* @param responseData response data string
|
||||||
|
* @throws IllegalArgumentException upon parsing error
|
||||||
|
* @return ResponseData object
|
||||||
|
*/
|
||||||
|
public static ResponseData parse(String responseData) {
|
||||||
|
// Must parse out main response data and response-specific data.
|
||||||
|
int index = responseData.indexOf(':');
|
||||||
|
String mainData, extraData;
|
||||||
|
if (-1 == index) {
|
||||||
|
mainData = responseData;
|
||||||
|
extraData = "";
|
||||||
|
} else {
|
||||||
|
mainData = responseData.substring(0, index);
|
||||||
|
extraData = index >= responseData.length() ? "" : responseData.substring(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
|
||||||
|
if (fields.length < 6) {
|
||||||
|
throw new IllegalArgumentException("Wrong number of fields.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseData data = new ResponseData();
|
||||||
|
data.extra = extraData;
|
||||||
|
data.responseCode = Integer.parseInt(fields[0]);
|
||||||
|
data.nonce = Integer.parseInt(fields[1]);
|
||||||
|
data.packageName = fields[2];
|
||||||
|
data.versionCode = fields[3];
|
||||||
|
// Application-specific user identifier.
|
||||||
|
data.userId = fields[4];
|
||||||
|
data.timestamp = Long.parseLong(fields[5]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return TextUtils.join("|", new Object[] {
|
||||||
|
responseCode, nonce, packageName, versionCode,
|
||||||
|
userId, timestamp });
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.vending.licensing.util.URIQueryDecoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default policy. All policy decisions are based off of response data received
|
||||||
|
* from the licensing service. Specifically, the licensing server sends the
|
||||||
|
* following information: response validity period, error retry period,
|
||||||
|
* error retry count and a URL for restoring app access in unlicensed cases.
|
||||||
|
* <p>
|
||||||
|
* These values will vary based on the the way the application is configured in
|
||||||
|
* the Google Play publishing console, such as whether the application is
|
||||||
|
* marked as free or is within its refund period, as well as how often an
|
||||||
|
* application is checking with the licensing service.
|
||||||
|
* <p>
|
||||||
|
* Developers who need more fine grained control over their application's
|
||||||
|
* licensing policy should implement a custom Policy.
|
||||||
|
*/
|
||||||
|
public class ServerManagedPolicy implements Policy {
|
||||||
|
|
||||||
|
private static final String TAG = "ServerManagedPolicy";
|
||||||
|
private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy";
|
||||||
|
private static final String PREF_LAST_RESPONSE = "lastResponse";
|
||||||
|
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
|
||||||
|
private static final String PREF_RETRY_UNTIL = "retryUntil";
|
||||||
|
private static final String PREF_MAX_RETRIES = "maxRetries";
|
||||||
|
private static final String PREF_RETRY_COUNT = "retryCount";
|
||||||
|
private static final String PREF_LICENSING_URL = "licensingUrl";
|
||||||
|
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
|
||||||
|
private static final String DEFAULT_RETRY_UNTIL = "0";
|
||||||
|
private static final String DEFAULT_MAX_RETRIES = "0";
|
||||||
|
private static final String DEFAULT_RETRY_COUNT = "0";
|
||||||
|
|
||||||
|
private static final long MILLIS_PER_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
|
private long mValidityTimestamp;
|
||||||
|
private long mRetryUntil;
|
||||||
|
private long mMaxRetries;
|
||||||
|
private long mRetryCount;
|
||||||
|
private long mLastResponseTime = 0;
|
||||||
|
private int mLastResponse;
|
||||||
|
private String mLicensingUrl;
|
||||||
|
private PreferenceObfuscator mPreferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context The context for the current application
|
||||||
|
* @param obfuscator An obfuscator to be used with preferences.
|
||||||
|
*/
|
||||||
|
public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
|
||||||
|
// Import old values
|
||||||
|
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
|
||||||
|
mPreferences = new PreferenceObfuscator(sp, obfuscator);
|
||||||
|
mLastResponse = Integer.parseInt(
|
||||||
|
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
|
||||||
|
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
|
||||||
|
DEFAULT_VALIDITY_TIMESTAMP));
|
||||||
|
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
|
||||||
|
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
|
||||||
|
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
|
||||||
|
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a new response from the license server.
|
||||||
|
* <p>
|
||||||
|
* This data will be used for computing future policy decisions. The
|
||||||
|
* following parameters are processed:
|
||||||
|
* <ul>
|
||||||
|
* <li>VT: the timestamp that the client should consider the response valid
|
||||||
|
* until
|
||||||
|
* <li>GT: the timestamp that the client should ignore retry errors until
|
||||||
|
* <li>GR: the number of retry errors that the client should ignore
|
||||||
|
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
|
||||||
|
* buy app on the Play Store)
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param response the result from validating the server response
|
||||||
|
* @param rawData the raw server response data
|
||||||
|
*/
|
||||||
|
public void processServerResponse(int response, ResponseData rawData) {
|
||||||
|
|
||||||
|
// Update retry counter
|
||||||
|
if (response != Policy.RETRY) {
|
||||||
|
setRetryCount(0);
|
||||||
|
} else {
|
||||||
|
setRetryCount(mRetryCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server policy data
|
||||||
|
Map<String, String> extras = decodeExtras(rawData);
|
||||||
|
if (response == Policy.LICENSED) {
|
||||||
|
mLastResponse = response;
|
||||||
|
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
|
||||||
|
setLicensingUrl(null);
|
||||||
|
setValidityTimestamp(extras.get("VT"));
|
||||||
|
setRetryUntil(extras.get("GT"));
|
||||||
|
setMaxRetries(extras.get("GR"));
|
||||||
|
} else if (response == Policy.NOT_LICENSED) {
|
||||||
|
// Clear out stale retry params
|
||||||
|
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
|
||||||
|
setRetryUntil(DEFAULT_RETRY_UNTIL);
|
||||||
|
setMaxRetries(DEFAULT_MAX_RETRIES);
|
||||||
|
// Update the licensing URL
|
||||||
|
setLicensingUrl(extras.get("LU"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastResponse(response);
|
||||||
|
mPreferences.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last license response received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param l the response
|
||||||
|
*/
|
||||||
|
private void setLastResponse(int l) {
|
||||||
|
mLastResponseTime = System.currentTimeMillis();
|
||||||
|
mLastResponse = l;
|
||||||
|
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current retry count and add to preferences. You must manually
|
||||||
|
* call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param c the new retry count
|
||||||
|
*/
|
||||||
|
private void setRetryCount(long c) {
|
||||||
|
mRetryCount = c;
|
||||||
|
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRetryCount() {
|
||||||
|
return mRetryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the last validity timestamp (VT) received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param validityTimestamp the VT string received
|
||||||
|
*/
|
||||||
|
private void setValidityTimestamp(String validityTimestamp) {
|
||||||
|
Long lValidityTimestamp;
|
||||||
|
try {
|
||||||
|
lValidityTimestamp = Long.parseLong(validityTimestamp);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parsable, expire in one minute.
|
||||||
|
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
|
||||||
|
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
|
||||||
|
validityTimestamp = Long.toString(lValidityTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
mValidityTimestamp = lValidityTimestamp;
|
||||||
|
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getValidityTimestamp() {
|
||||||
|
return mValidityTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the retry until timestamp (GT) received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param retryUntil the GT string received
|
||||||
|
*/
|
||||||
|
private void setRetryUntil(String retryUntil) {
|
||||||
|
Long lRetryUntil;
|
||||||
|
try {
|
||||||
|
lRetryUntil = Long.parseLong(retryUntil);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parsable, expire immediately
|
||||||
|
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
|
||||||
|
retryUntil = "0";
|
||||||
|
lRetryUntil = 0l;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRetryUntil = lRetryUntil;
|
||||||
|
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRetryUntil() {
|
||||||
|
return mRetryUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the max retries value (GR) as received from the server and add to
|
||||||
|
* preferences. You must manually call PreferenceObfuscator.commit() to
|
||||||
|
* commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param maxRetries the GR string received
|
||||||
|
*/
|
||||||
|
private void setMaxRetries(String maxRetries) {
|
||||||
|
Long lMaxRetries;
|
||||||
|
try {
|
||||||
|
lMaxRetries = Long.parseLong(maxRetries);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// No response or not parsable, expire immediately
|
||||||
|
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
|
||||||
|
maxRetries = "0";
|
||||||
|
lMaxRetries = 0l;
|
||||||
|
}
|
||||||
|
|
||||||
|
mMaxRetries = lMaxRetries;
|
||||||
|
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxRetries() {
|
||||||
|
return mMaxRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the license URL value (LU) as received from the server and add to preferences. You must
|
||||||
|
* manually call PreferenceObfuscator.commit() to commit these changes to disk.
|
||||||
|
*
|
||||||
|
* @param url the LU string received
|
||||||
|
*/
|
||||||
|
private void setLicensingUrl(String url) {
|
||||||
|
mLicensingUrl = url;
|
||||||
|
mPreferences.putString(PREF_LICENSING_URL, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLicensingUrl() {
|
||||||
|
return mLicensingUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* This implementation allows access if either:<br>
|
||||||
|
* <ol>
|
||||||
|
* <li>a LICENSED response was received within the validity period
|
||||||
|
* <li>a RETRY response was received in the last minute, and we are under
|
||||||
|
* the RETRY count or in the RETRY period.
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
public boolean allowAccess() {
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
if (mLastResponse == Policy.LICENSED) {
|
||||||
|
// Check if the LICENSED response occurred within the validity timeout.
|
||||||
|
if (ts <= mValidityTimestamp) {
|
||||||
|
// Cached LICENSED response is still valid.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (mLastResponse == Policy.RETRY &&
|
||||||
|
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
|
||||||
|
// Only allow access if we are within the retry period or we haven't used up our
|
||||||
|
// max retries.
|
||||||
|
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> decodeExtras(
|
||||||
|
com.google.android.vending.licensing.ResponseData rawData) {
|
||||||
|
Map<String, String> results = new HashMap<String, String>();
|
||||||
|
if (rawData == null) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI rawExtras = new URI("?" + rawData.extra);
|
||||||
|
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,13 @@
|
|||||||
|
|
||||||
package com.google.android.vending.licensing;
|
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,
|
* Non-caching policy. All requests will be sent to the licensing service,
|
||||||
* and no local caching is performed.
|
* 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,
|
* weigh the risks of using this Policy over one which implements caching,
|
||||||
* such as ServerManagedPolicy.
|
* such as ServerManagedPolicy.
|
||||||
* <p>
|
* <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.
|
* received. All other responses (including RETRY) will deny access.
|
||||||
*/
|
*/
|
||||||
public class StrictPolicy implements Policy {
|
public class StrictPolicy implements Policy {
|
||||||
|
|
||||||
private int mLastResponse;
|
private static final String TAG = "StrictPolicy";
|
||||||
|
|
||||||
public StrictPolicy() {
|
private int mLastResponse;
|
||||||
// Set default policy. This will force the application to check the policy on launch.
|
private String mLicensingUrl;
|
||||||
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
|
* Process a new response from the license server. Since we aren't
|
||||||
* performing any caching, this equates to reading the LicenseResponse.
|
* 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 response the result from validating the server response
|
||||||
* @param rawData the raw server response data
|
* @param rawData the raw server response data
|
||||||
*/
|
*/
|
||||||
public void processServerResponse(int response, ResponseData rawData) {
|
public void processServerResponse(int response, ResponseData rawData) {
|
||||||
mLastResponse = response;
|
mLastResponse = response;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (response == Policy.NOT_LICENSED) {
|
||||||
|
Map<String, String> extras = decodeExtras(rawData);
|
||||||
|
mLicensingUrl = extras.get("LU");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* This implementation allows access if and only if a LICENSED response
|
* This implementation allows access if and only if a LICENSED response
|
||||||
* was received the last time the server was contacted.
|
* was received the last time the server was contacted.
|
||||||
*/
|
*/
|
||||||
public boolean allowAccess() {
|
public boolean allowAccess() {
|
||||||
return (mLastResponse == Policy.LICENSED);
|
return (mLastResponse == Policy.LICENSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLicensingUrl() {
|
||||||
|
return mLicensingUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> decodeExtras(
|
||||||
|
com.google.android.vending.licensing.ResponseData rawData) {
|
||||||
|
Map<String, String> results = new HashMap<String, String>();
|
||||||
|
if (rawData == null) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI rawExtras = new URI("?" + rawData.extra);
|
||||||
|
URIQueryDecoder.DecodeQuery(rawExtras, results);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
@ -21,13 +21,13 @@ package com.google.android.vending.licensing;
|
|||||||
* {@link Obfuscator}.}
|
* {@link Obfuscator}.}
|
||||||
*/
|
*/
|
||||||
public class ValidationException extends Exception {
|
public class ValidationException extends Exception {
|
||||||
public ValidationException() {
|
public ValidationException() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidationException(String s) {
|
public ValidationException(String s) {
|
||||||
super(s);
|
super(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
}
|
}
|
@ -0,0 +1,556 @@
|
|||||||
|
// Portions copyright 2002, Google, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing.util;
|
||||||
|
|
||||||
|
// This code was converted from code at http://iharder.sourceforge.net/base64/
|
||||||
|
// Lots of extraneous features were removed.
|
||||||
|
/* The original code said:
|
||||||
|
* <p>
|
||||||
|
* I am placing this code in the Public Domain. Do with it as you will.
|
||||||
|
* This software comes with no guarantees or warranties but with
|
||||||
|
* plenty of well-wishing instead!
|
||||||
|
* Please visit
|
||||||
|
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
|
||||||
|
* periodically to check for updates or to contribute improvements.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Robert Harder
|
||||||
|
* @author rharder@usa.net
|
||||||
|
* @version 1.3
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.godot.game.BuildConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 converter class. This code is not a full-blown MIME encoder;
|
||||||
|
* it simply converts binary data to base64 data and back.
|
||||||
|
*
|
||||||
|
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
public class Base64 {
|
||||||
|
/** Specify encoding (value is {@code true}). */
|
||||||
|
public final static boolean ENCODE = true;
|
||||||
|
|
||||||
|
/** Specify decoding (value is {@code false}). */
|
||||||
|
public final static boolean DECODE = false;
|
||||||
|
|
||||||
|
/** The equals sign (=) as a byte. */
|
||||||
|
private final static byte EQUALS_SIGN = (byte)'=';
|
||||||
|
|
||||||
|
/** The new line character (\n) as a byte. */
|
||||||
|
private final static byte NEW_LINE = (byte)'\n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F',
|
||||||
|
(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K',
|
||||||
|
(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
|
||||||
|
(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
|
||||||
|
(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
|
||||||
|
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e',
|
||||||
|
(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j',
|
||||||
|
(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o',
|
||||||
|
(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t',
|
||||||
|
(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y',
|
||||||
|
(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
|
||||||
|
(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8',
|
||||||
|
(byte)'9', (byte)'+', (byte)'/' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 64 valid web safe Base64 values.
|
||||||
|
*/
|
||||||
|
private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F',
|
||||||
|
(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K',
|
||||||
|
(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
|
||||||
|
(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
|
||||||
|
(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
|
||||||
|
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e',
|
||||||
|
(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j',
|
||||||
|
(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o',
|
||||||
|
(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t',
|
||||||
|
(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y',
|
||||||
|
(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
|
||||||
|
(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8',
|
||||||
|
(byte)'9', (byte)'-', (byte)'_' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a Base64 value to either its 6-bit reconstruction value
|
||||||
|
* or a negative number indicating some other meaning.
|
||||||
|
**/
|
||||||
|
private final static byte[] DECODABET = {
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
|
||||||
|
62, // Plus sign at decimal 43
|
||||||
|
-9, -9, -9, // Decimal 44 - 46
|
||||||
|
63, // Slash at decimal 47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The web safe decodabet */
|
||||||
|
private final static byte[] WEBSAFE_DECODABET = {
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
|
||||||
|
-5, -5, // Whitespace: Tab and Linefeed
|
||||||
|
-9, -9, // Decimal 11 - 12
|
||||||
|
-5, // Whitespace: Carriage Return
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
|
||||||
|
-9, -9, -9, -9, -9, // Decimal 27 - 31
|
||||||
|
-5, // Whitespace: Space
|
||||||
|
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
|
||||||
|
62, // Dash '-' sign at decimal 45
|
||||||
|
-9, -9, // Decimal 46-47
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
|
||||||
|
-9, -9, -9, // Decimal 58 - 60
|
||||||
|
-1, // Equals sign at decimal 61
|
||||||
|
-9, -9, -9, // Decimal 62 - 64
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
|
||||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
|
||||||
|
-9, -9, -9, -9, // Decimal 91-94
|
||||||
|
63, // Underscore '_' at decimal 95
|
||||||
|
-9, // Decimal 96
|
||||||
|
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
|
||||||
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
|
||||||
|
-9, -9, -9, -9, -9 // Decimal 123 - 127
|
||||||
|
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
|
||||||
|
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicates white space in encoding
|
||||||
|
private final static byte WHITE_SPACE_ENC = -5;
|
||||||
|
// Indicates equals sign in encoding
|
||||||
|
private final static byte EQUALS_SIGN_ENC = -1;
|
||||||
|
|
||||||
|
/** Defeats instantiation. */
|
||||||
|
private Base64() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******** E N C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes up to three bytes of the array <var>source</var>
|
||||||
|
* and writes the resulting four Base64 bytes to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 3 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 4 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* The actual number of significant bytes in your array is
|
||||||
|
* given by <var>numSigBytes</var>.
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param numSigBytes the number of significant bytes in your array
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @return the <var>destination</var> array
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static byte[] encode3to4(byte[] source, int srcOffset,
|
||||||
|
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
|
||||||
|
// 1 2 3
|
||||||
|
// 01234567890123456789012345678901 Bit position
|
||||||
|
// --------000000001111111122222222 Array position from threeBytes
|
||||||
|
// --------| || || || | Six bit groups to index alphabet
|
||||||
|
// >>18 >>12 >> 6 >> 0 Right shift necessary
|
||||||
|
// 0x3f 0x3f 0x3f Additional AND
|
||||||
|
|
||||||
|
// Create buffer with zero-padding if there are only one or two
|
||||||
|
// significant bytes passed in the array.
|
||||||
|
// We have to shift left 24 in order to flush out the 1's that appear
|
||||||
|
// when Java treats a value as negative that is cast from a byte to an int.
|
||||||
|
int inBuff =
|
||||||
|
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
|
||||||
|
|
||||||
|
switch (numSigBytes) {
|
||||||
|
case 3:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = alphabet[(inBuff)&0x3f];
|
||||||
|
return destination;
|
||||||
|
case 2:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
case 1:
|
||||||
|
destination[destOffset] = alphabet[(inBuff >>> 18)];
|
||||||
|
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
destination[destOffset + 2] = EQUALS_SIGN;
|
||||||
|
destination[destOffset + 3] = EQUALS_SIGN;
|
||||||
|
return destination;
|
||||||
|
default:
|
||||||
|
return destination;
|
||||||
|
} // end switch
|
||||||
|
} // end encode3to4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
* Equivalent to calling
|
||||||
|
* {@code encodeBytes(source, 0, source.length)}
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source) {
|
||||||
|
return encode(source, 0, source.length, ALPHABET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into web safe Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
*/
|
||||||
|
public static String encodeWebSafe(byte[] source, boolean doPadding) {
|
||||||
|
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @param off Offset in array where conversion should begin
|
||||||
|
* @param len Length of data to convert
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @param doPadding is {@code true} to pad result with '=' chars
|
||||||
|
* if it does not fall on 3 byte boundaries
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static String encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
boolean doPadding) {
|
||||||
|
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
|
||||||
|
int outLen = outBuff.length;
|
||||||
|
|
||||||
|
// If doPadding is false, set length to truncate '='
|
||||||
|
// padding characters
|
||||||
|
while (doPadding == false && outLen > 0) {
|
||||||
|
if (outBuff[outLen - 1] != '=') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
outLen -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(outBuff, 0, outLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a byte array into Base64 notation.
|
||||||
|
*
|
||||||
|
* @param source The data to convert
|
||||||
|
* @param off Offset in array where conversion should begin
|
||||||
|
* @param len Length of data to convert
|
||||||
|
* @param alphabet is the encoding alphabet
|
||||||
|
* @param maxLineLength maximum length of one line.
|
||||||
|
* @return the BASE64-encoded byte array
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
|
||||||
|
int maxLineLength) {
|
||||||
|
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
|
||||||
|
int len43 = lenDiv3 * 4;
|
||||||
|
byte[] outBuff = new byte[len43 // Main 4:3
|
||||||
|
+ (len43 / maxLineLength)]; // New lines
|
||||||
|
|
||||||
|
int d = 0;
|
||||||
|
int e = 0;
|
||||||
|
int len2 = len - 2;
|
||||||
|
int lineLength = 0;
|
||||||
|
for (; d < len2; d += 3, e += 4) {
|
||||||
|
|
||||||
|
// The following block of code is the same as
|
||||||
|
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
|
||||||
|
// but inlined for faster encoding (~20% improvement)
|
||||||
|
int inBuff =
|
||||||
|
((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24);
|
||||||
|
outBuff[e] = alphabet[(inBuff >>> 18)];
|
||||||
|
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
|
||||||
|
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
|
||||||
|
outBuff[e + 3] = alphabet[(inBuff)&0x3f];
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
lineLength = 0;
|
||||||
|
} // end if: end of line
|
||||||
|
} // end for: each piece of array
|
||||||
|
|
||||||
|
if (d < len) {
|
||||||
|
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
|
||||||
|
|
||||||
|
lineLength += 4;
|
||||||
|
if (lineLength == maxLineLength) {
|
||||||
|
// Add a last newline
|
||||||
|
outBuff[e + 4] = NEW_LINE;
|
||||||
|
e++;
|
||||||
|
}
|
||||||
|
e += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG && e != outBuff.length)
|
||||||
|
throw new RuntimeException();
|
||||||
|
return outBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******** D E C O D I N G M E T H O D S ******** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes four bytes from array <var>source</var>
|
||||||
|
* and writes the resulting bytes (up to three of them)
|
||||||
|
* to <var>destination</var>.
|
||||||
|
* The source and destination arrays can be manipulated
|
||||||
|
* anywhere along their length by specifying
|
||||||
|
* <var>srcOffset</var> and <var>destOffset</var>.
|
||||||
|
* This method does not check to make sure your arrays
|
||||||
|
* are large enough to accommodate <var>srcOffset</var> + 4 for
|
||||||
|
* the <var>source</var> array or <var>destOffset</var> + 3 for
|
||||||
|
* the <var>destination</var> array.
|
||||||
|
* This method returns the actual number of bytes that
|
||||||
|
* were converted from the Base64 encoding.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param source the array to convert
|
||||||
|
* @param srcOffset the index where conversion begins
|
||||||
|
* @param destination the array to hold the conversion
|
||||||
|
* @param destOffset the index where output will be put
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return the number of decoded bytes converted
|
||||||
|
* @since 1.3
|
||||||
|
*/
|
||||||
|
private static int decode4to3(byte[] source, int srcOffset,
|
||||||
|
byte[] destination, int destOffset, byte[] decodabet) {
|
||||||
|
// Example: Dk==
|
||||||
|
if (source[srcOffset + 2] == EQUALS_SIGN) {
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte)(outBuff >>> 16);
|
||||||
|
return 1;
|
||||||
|
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
|
||||||
|
// Example: DkL=
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte)(outBuff >>> 16);
|
||||||
|
destination[destOffset + 1] = (byte)(outBuff >>> 8);
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
// Example: DkLE
|
||||||
|
int outBuff =
|
||||||
|
((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
|
||||||
|
|
||||||
|
destination[destOffset] = (byte)(outBuff >> 16);
|
||||||
|
destination[destOffset + 1] = (byte)(outBuff >> 8);
|
||||||
|
destination[destOffset + 2] = (byte)(outBuff);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
} // end decodeToBytes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from Base64 notation.
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
* @since 1.4
|
||||||
|
*/
|
||||||
|
public static byte[] decode(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decode(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data from web safe Base64 notation.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param s the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
|
||||||
|
byte[] bytes = s.getBytes();
|
||||||
|
return decodeWebSafe(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source) throws Base64DecoderException {
|
||||||
|
return decode(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded data.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source the string to decode (decoded in default encoding)
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decodeWebSafe(source, 0, source.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @param off The offset of where to begin decoding
|
||||||
|
* @param len The length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
* @since 1.3
|
||||||
|
* @throws Base64DecoderException
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes web safe Base64 content in byte array format and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @param off The offset of where to begin decoding
|
||||||
|
* @param len The length of characters to decode
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decodeWebSafe(byte[] source, int off, int len)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
return decode(source, off, len, WEBSAFE_DECODABET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes Base64 content using the supplied decodabet and returns
|
||||||
|
* the decoded byte array.
|
||||||
|
*
|
||||||
|
* @param source The Base64 encoded data
|
||||||
|
* @param off The offset of where to begin decoding
|
||||||
|
* @param len The length of characters to decode
|
||||||
|
* @param decodabet the decodabet for decoding Base64 content
|
||||||
|
* @return decoded data
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
|
||||||
|
throws Base64DecoderException {
|
||||||
|
int len34 = len * 3 / 4;
|
||||||
|
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
|
||||||
|
int outBuffPosn = 0;
|
||||||
|
|
||||||
|
byte[] b4 = new byte[4];
|
||||||
|
int b4Posn = 0;
|
||||||
|
int i = 0;
|
||||||
|
byte sbiCrop = 0;
|
||||||
|
byte sbiDecode = 0;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits
|
||||||
|
sbiDecode = decodabet[sbiCrop];
|
||||||
|
|
||||||
|
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
|
||||||
|
if (sbiDecode >= EQUALS_SIGN_ENC) {
|
||||||
|
// An equals sign (for padding) must not occur at position 0 or 1
|
||||||
|
// and must be the last byte[s] in the encoded value
|
||||||
|
if (sbiCrop == EQUALS_SIGN) {
|
||||||
|
int bytesLeft = len - i;
|
||||||
|
byte lastByte = (byte)(source[len - 1 + off] & 0x7f);
|
||||||
|
if (b4Posn == 0 || b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"invalid padding byte '=' at byte offset " + i);
|
||||||
|
} else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"padding byte '=' falsely signals end of encoded value "
|
||||||
|
+ "at offset " + i);
|
||||||
|
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
|
||||||
|
throw new Base64DecoderException(
|
||||||
|
"encoded value has invalid trailing byte");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
b4[b4Posn++] = sbiCrop;
|
||||||
|
if (b4Posn == 4) {
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
b4Posn = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because web safe encoding allows non padding base64 encodes, we
|
||||||
|
// need to pad the rest of the b4 buffer with equal signs when
|
||||||
|
// b4Posn != 0. There can be at most 2 equal signs at the end of
|
||||||
|
// four characters, so the b4 buffer must have two or three
|
||||||
|
// characters. This also catches the case where the input is
|
||||||
|
// padded with EQUALS_SIGN
|
||||||
|
if (b4Posn != 0) {
|
||||||
|
if (b4Posn == 1) {
|
||||||
|
throw new Base64DecoderException("single trailing character at offset " + (len - 1));
|
||||||
|
}
|
||||||
|
b4[b4Posn++] = EQUALS_SIGN;
|
||||||
|
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] out = new byte[outBuffPosn];
|
||||||
|
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
@ -20,13 +20,13 @@ package com.google.android.vending.licensing.util;
|
|||||||
* @author nelson
|
* @author nelson
|
||||||
*/
|
*/
|
||||||
public class Base64DecoderException extends Exception {
|
public class Base64DecoderException extends Exception {
|
||||||
public Base64DecoderException() {
|
public Base64DecoderException() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Base64DecoderException(String s) {
|
public Base64DecoderException(String s) {
|
||||||
super(s);
|
super(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
}
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.vending.licensing.util;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class URIQueryDecoder {
|
||||||
|
private static final String TAG = "URIQueryDecoder";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the query portion of the passed-in URI.
|
||||||
|
*
|
||||||
|
* @param encodedURI the URI containing the query to decode
|
||||||
|
* @param results a map containing all query parameters. Query parameters that do not have a
|
||||||
|
* value will map to a null string
|
||||||
|
*/
|
||||||
|
static public void DecodeQuery(URI encodedURI, Map<String, String> results) {
|
||||||
|
Scanner scanner = new Scanner(encodedURI.getRawQuery());
|
||||||
|
scanner.useDelimiter("&");
|
||||||
|
try {
|
||||||
|
while (scanner.hasNext()) {
|
||||||
|
String param = scanner.next();
|
||||||
|
String[] valuePair = param.split("=");
|
||||||
|
String name, value;
|
||||||
|
if (valuePair.length == 1) {
|
||||||
|
value = null;
|
||||||
|
} else if (valuePair.length == 2) {
|
||||||
|
value = URLDecoder.decode(valuePair[1], "UTF-8");
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("query parameter invalid");
|
||||||
|
}
|
||||||
|
name = URLDecoder.decode(valuePair[0], "UTF-8");
|
||||||
|
results.put(name, value);
|
||||||
|
}
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// This should never happen.
|
||||||
|
Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,59 +30,48 @@
|
|||||||
|
|
||||||
package org.godotengine.godot;
|
package org.godotengine.godot;
|
||||||
|
|
||||||
import android.R;
|
//import android.R;
|
||||||
|
|
||||||
import android.app.Activity;
|
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.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.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.MotionEvent;
|
||||||
import android.view.View;
|
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.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
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.DownloadProgressInfo;
|
||||||
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
|
||||||
import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
|
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.IDownloaderService;
|
||||||
import com.google.android.vending.expansion.downloader.IStub;
|
import com.google.android.vending.expansion.downloader.IStub;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import org.godotengine.godot.input.GodotEditText;
|
||||||
import android.os.Messenger;
|
import org.godotengine.godot.payments.PaymentsManager;
|
||||||
import android.os.SystemClock;
|
|
||||||
|
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 {
|
public class Godot extends Activity implements SensorEventListener, IDownloaderClient {
|
||||||
|
|
||||||
@ -222,9 +222,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
private Sensor mGyroscope;
|
private Sensor mGyroscope;
|
||||||
|
|
||||||
public FrameLayout layout;
|
public FrameLayout layout;
|
||||||
public RelativeLayout adLayout;
|
|
||||||
|
|
||||||
static public GodotIO io;
|
public static GodotIO io;
|
||||||
|
|
||||||
public static void setWindowTitle(String title) {
|
public static void setWindowTitle(String title) {
|
||||||
//setTitle(title);
|
//setTitle(title);
|
||||||
@ -263,24 +262,23 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
};
|
};
|
||||||
|
|
||||||
public void onVideoInit() {
|
public void onVideoInit() {
|
||||||
|
|
||||||
boolean use_gl3 = getGLESVersionCode() >= 0x00030000;
|
boolean use_gl3 = getGLESVersionCode() >= 0x00030000;
|
||||||
|
|
||||||
//mView = new GodotView(getApplication(),io,use_gl3);
|
//mView = new GodotView(getApplication(),io,use_gl3);
|
||||||
//setContentView(mView);
|
//setContentView(mView);
|
||||||
|
|
||||||
layout = new FrameLayout(this);
|
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);
|
setContentView(layout);
|
||||||
|
|
||||||
// GodotEditText layout
|
// GodotEditText layout
|
||||||
GodotEditText edittext = new GodotEditText(this);
|
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
|
// ...add to FrameLayout
|
||||||
layout.addView(edittext);
|
layout.addView(edittext);
|
||||||
|
|
||||||
mView = new GodotView(getApplication(), io, use_gl3, use_32_bits, use_debug_opengl, this);
|
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);
|
edittext.setView(mView);
|
||||||
io.setEdit(edittext);
|
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 String[] current_command_line = command_line;
|
||||||
final GodotView view = mView;
|
final GodotView view = mView;
|
||||||
mView.queueEvent(new Runnable() {
|
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) {
|
public void alert(final String message, final String title) {
|
||||||
|
final Activity activity = this;
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getInstance());
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
builder.setMessage(message).setTitle(title);
|
builder.setMessage(message).setTitle(title);
|
||||||
builder.setPositiveButton(
|
builder.setPositiveButton(
|
||||||
"OK",
|
"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() {
|
public int getGLESVersionCode() {
|
||||||
ActivityManager am = (ActivityManager)Godot.getInstance().getSystemService(Context.ACTIVITY_SERVICE);
|
ActivityManager am = (ActivityManager)this.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo();
|
ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo();
|
||||||
return deviceInfo.reqGlEsVersion;
|
return deviceInfo.reqGlEsVersion;
|
||||||
}
|
}
|
||||||
@ -421,7 +409,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
}
|
}
|
||||||
|
|
||||||
io = new GodotIO(this);
|
io = new GodotIO(this);
|
||||||
io.unique_id = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
|
io.unique_id = Secure.ANDROID_ID;
|
||||||
GodotLib.io = io;
|
GodotLib.io = io;
|
||||||
mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
|
mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
|
||||||
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||||
@ -452,7 +440,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
protected void onCreate(Bundle icicle) {
|
protected void onCreate(Bundle icicle) {
|
||||||
|
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
_self = this;
|
|
||||||
Window window = getWindow();
|
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 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_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;
|
use_debug_opengl = true;
|
||||||
} else if (command_line[i].equals("--use_immersive")) {
|
} else if (command_line[i].equals("--use_immersive")) {
|
||||||
use_immersive = true;
|
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(
|
window.getDecorView().setSystemUiVisibility(
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
@ -498,7 +485,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
Editor editor = prefs.edit();
|
Editor editor = prefs.edit();
|
||||||
editor.putString("store_public_key", main_pack_key);
|
editor.putString("store_public_key", main_pack_key);
|
||||||
|
|
||||||
editor.commit();
|
editor.apply();
|
||||||
i++;
|
i++;
|
||||||
} else if (command_line[i].trim().length() != 0) {
|
} else if (command_line[i].trim().length() != 0) {
|
||||||
new_args.add(command_line[i]);
|
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, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
|
||||||
mSensorManager.registerListener(this, mGyroscope, 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 window = getWindow();
|
||||||
window.getDecorView().setSystemUiVisibility(
|
window.getDecorView().setSystemUiVisibility(
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
@ -688,13 +675,15 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC
|
|||||||
@Override
|
@Override
|
||||||
public void onSystemUiVisibilityChange(int visibility) {
|
public void onSystemUiVisibilityChange(int visibility) {
|
||||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||||
decorView.setSystemUiVisibility(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
decorView.setSystemUiVisibility(
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
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,
|
mTimeRemaining.setText(getString(com.godot.game.R.string.time_remaining,
|
||||||
Helpers.getTimeRemaining(progress.mTimeRemaining)));
|
Helpers.getTimeRemaining(progress.mTimeRemaining)));
|
||||||
|
|
||||||
progress.mOverallTotal = progress.mOverallTotal;
|
|
||||||
mPB.setMax((int)(progress.mOverallTotal >> 8));
|
mPB.setMax((int)(progress.mOverallTotal >> 8));
|
||||||
mPB.setProgress((int)(progress.mOverallProgress >> 8));
|
mPB.setProgress((int)(progress.mOverallProgress >> 8));
|
||||||
mProgressPercent.setText(Long.toString(progress.mOverallProgress * 100 /
|
mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal));
|
||||||
progress.mOverallTotal) +
|
|
||||||
"%");
|
|
||||||
mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
|
mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
|
||||||
progress.mOverallTotal));
|
progress.mOverallTotal));
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import java.io.InputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import android.app.*;
|
import android.app.*;
|
||||||
import android.content.*;
|
import android.content.*;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.os.*;
|
import android.os.*;
|
||||||
@ -61,7 +62,6 @@ public class GodotIO {
|
|||||||
Godot activity;
|
Godot activity;
|
||||||
GodotEditText edit;
|
GodotEditText edit;
|
||||||
|
|
||||||
Context applicationContext;
|
|
||||||
MediaPlayer mediaPlayer;
|
MediaPlayer mediaPlayer;
|
||||||
|
|
||||||
final int SCREEN_LANDSCAPE = 0;
|
final int SCREEN_LANDSCAPE = 0;
|
||||||
@ -87,7 +87,7 @@ public class GodotIO {
|
|||||||
public int pos;
|
public int pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
HashMap<Integer, AssetData> streams;
|
SparseArray<AssetData> streams;
|
||||||
|
|
||||||
public int file_open(String path, boolean write) {
|
public int file_open(String path, boolean write) {
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
public int file_get_size(int id) {
|
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);
|
System.out.printf("file_get_size: Invalid file id: %d\n", id);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
public void file_seek(int id, int bytes) {
|
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);
|
System.out.printf("file_get_size: Invalid file id: %d\n", id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -174,7 +174,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public int file_tell(int id) {
|
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);
|
System.out.printf("file_read: Can't tell eof for invalid file id: %d\n", id);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
public boolean file_eof(int id) {
|
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);
|
System.out.printf("file_read: Can't check eof for invalid file id: %d\n", id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public byte[] file_read(int id, int bytes) {
|
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);
|
System.out.printf("file_read: Can't read invalid file id: %d\n", id);
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
@ -243,7 +243,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public void file_close(int id) {
|
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);
|
System.out.printf("file_close: Can't close invalid file id: %d\n", id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -264,7 +264,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public int last_dir_id = 1;
|
public int last_dir_id = 1;
|
||||||
|
|
||||||
HashMap<Integer, AssetDir> dirs;
|
SparseArray<AssetDir> dirs;
|
||||||
|
|
||||||
public int dir_open(String path) {
|
public int dir_open(String path) {
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean dir_is_dir(int id) {
|
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);
|
System.out.printf("dir_next: invalid dir id: %d\n", id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -320,7 +320,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public String dir_next(int id) {
|
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);
|
System.out.printf("dir_next: invalid dir id: %d\n", id);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -339,7 +339,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
public void dir_close(int id) {
|
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);
|
System.out.printf("dir_close: invalid dir id: %d\n", id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -351,9 +351,9 @@ public class GodotIO {
|
|||||||
|
|
||||||
am = p_activity.getAssets();
|
am = p_activity.getAssets();
|
||||||
activity = p_activity;
|
activity = p_activity;
|
||||||
streams = new HashMap<Integer, AssetData>();
|
//streams = new HashMap<Integer, AssetData>();
|
||||||
dirs = new HashMap<Integer, AssetDir>();
|
streams = new SparseArray<AssetData>();
|
||||||
applicationContext = activity.getApplicationContext();
|
dirs = new SparseArray<AssetDir>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////
|
/////////////////////////
|
||||||
@ -365,7 +365,7 @@ public class GodotIO {
|
|||||||
private AudioTrack mAudioTrack;
|
private AudioTrack mAudioTrack;
|
||||||
|
|
||||||
public Object audioInit(int sampleRate, int desiredFrames) {
|
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 audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
int frameSize = 4;
|
int frameSize = 4;
|
||||||
|
|
||||||
@ -496,13 +496,13 @@ public class GodotIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getScreenDPI() {
|
public int getScreenDPI() {
|
||||||
DisplayMetrics metrics = applicationContext.getResources().getDisplayMetrics();
|
DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics();
|
||||||
return (int)(metrics.density * 160f);
|
return (int)(metrics.density * 160f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean needsReloadHooks() {
|
public boolean needsReloadHooks() {
|
||||||
|
|
||||||
return android.os.Build.VERSION.SDK_INT < 11;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showKeyboard(String p_existing_text) {
|
public void showKeyboard(String p_existing_text) {
|
||||||
@ -564,7 +564,7 @@ public class GodotIO {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||||
mediaPlayer.setDataSource(applicationContext, filePath);
|
mediaPlayer.setDataSource(activity.getApplicationContext(), filePath);
|
||||||
mediaPlayer.prepare();
|
mediaPlayer.prepare();
|
||||||
mediaPlayer.start();
|
mediaPlayer.start();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
/*************************************************************************/
|
/*************************************************************************/
|
||||||
|
|
||||||
package org.godotengine.godot;
|
package org.godotengine.godot;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.opengl.GLSurfaceView;
|
import android.opengl.GLSurfaceView;
|
||||||
@ -75,9 +76,9 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener {
|
|||||||
|
|
||||||
private static String TAG = "GodotView";
|
private static String TAG = "GodotView";
|
||||||
private static final boolean DEBUG = false;
|
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 firsttime = true;
|
||||||
private static boolean use_gl3 = false;
|
private static boolean use_gl3 = false;
|
||||||
private static boolean use_32 = false;
|
private static boolean use_32 = false;
|
||||||
@ -105,20 +106,26 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener {
|
|||||||
init(false, 16, 0);
|
init(false, 16, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GodotView(Context context) {
|
||||||
|
super(context);
|
||||||
|
ctx = context;
|
||||||
|
}
|
||||||
|
|
||||||
public GodotView(Context context, boolean translucent, int depth, int stencil) {
|
public GodotView(Context context, boolean translucent, int depth, int stencil) {
|
||||||
super(context);
|
super(context);
|
||||||
init(translucent, depth, stencil);
|
init(translucent, depth, stencil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
super.onTouchEvent(event);
|
super.onTouchEvent(event);
|
||||||
return activity.gotTouchEvent(event);
|
return activity.gotTouchEvent(event);
|
||||||
};
|
}
|
||||||
|
|
||||||
public int get_godot_button(int keyCode) {
|
public int get_godot_button(int keyCode) {
|
||||||
|
|
||||||
int button = 0;
|
int button;
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B
|
case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B
|
||||||
button = 0;
|
button = 0;
|
||||||
@ -178,7 +185,7 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener {
|
|||||||
default:
|
default:
|
||||||
button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20;
|
button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20;
|
||||||
break;
|
break;
|
||||||
};
|
}
|
||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -440,6 +447,10 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener {
|
|||||||
private static class ContextFactory implements GLSurfaceView.EGLContextFactory {
|
private static class ContextFactory implements GLSurfaceView.EGLContextFactory {
|
||||||
private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||||
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
|
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)
|
if (use_gl3)
|
||||||
Log.w(TAG, "creating OpenGL ES 3.0 context :");
|
Log.w(TAG, "creating OpenGL ES 3.0 context :");
|
||||||
else
|
else
|
||||||
@ -508,26 +519,24 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener {
|
|||||||
* perform actual matching in chooseConfig() below.
|
* perform actual matching in chooseConfig() below.
|
||||||
*/
|
*/
|
||||||
private static int EGL_OPENGL_ES2_BIT = 4;
|
private static int EGL_OPENGL_ES2_BIT = 4;
|
||||||
private static int[] s_configAttribs2 =
|
private static int[] s_configAttribs2 = {
|
||||||
{
|
EGL10.EGL_RED_SIZE, 4,
|
||||||
EGL10.EGL_RED_SIZE, 4,
|
EGL10.EGL_GREEN_SIZE, 4,
|
||||||
EGL10.EGL_GREEN_SIZE, 4,
|
EGL10.EGL_BLUE_SIZE, 4,
|
||||||
EGL10.EGL_BLUE_SIZE, 4,
|
// EGL10.EGL_DEPTH_SIZE, 16,
|
||||||
// EGL10.EGL_DEPTH_SIZE, 16,
|
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
|
||||||
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
|
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||||
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
EGL10.EGL_NONE
|
||||||
EGL10.EGL_NONE
|
};
|
||||||
};
|
private static int[] s_configAttribs3 = {
|
||||||
private static int[] s_configAttribs3 =
|
EGL10.EGL_RED_SIZE, 4,
|
||||||
{
|
EGL10.EGL_GREEN_SIZE, 4,
|
||||||
EGL10.EGL_RED_SIZE, 4,
|
EGL10.EGL_BLUE_SIZE, 4,
|
||||||
EGL10.EGL_GREEN_SIZE, 4,
|
// EGL10.EGL_DEPTH_SIZE, 16,
|
||||||
EGL10.EGL_BLUE_SIZE, 4,
|
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
|
||||||
// EGL10.EGL_DEPTH_SIZE, 16,
|
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT
|
||||||
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
|
EGL10.EGL_NONE
|
||||||
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) {
|
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ import android.os.Message;
|
|||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class GodotEditText extends EditText {
|
public class GodotEditText extends EditText {
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
// Constants
|
// Constants
|
||||||
@ -51,9 +53,24 @@ public class GodotEditText extends EditText {
|
|||||||
// ===========================================================
|
// ===========================================================
|
||||||
private GodotView mView;
|
private GodotView mView;
|
||||||
private GodotTextInputWrapper mInputWrapper;
|
private GodotTextInputWrapper mInputWrapper;
|
||||||
private static Handler sHandler;
|
private EditHandler sHandler = new EditHandler(this);
|
||||||
private String mOriginText;
|
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
|
// Constructors
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
@ -75,36 +92,33 @@ public class GodotEditText extends EditText {
|
|||||||
protected void initView() {
|
protected void initView() {
|
||||||
this.setPadding(0, 0, 0, 0);
|
this.setPadding(0, 0, 0, 0);
|
||||||
this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||||
|
}
|
||||||
|
|
||||||
sHandler = new Handler() {
|
private void handleMessage(final Message msg) {
|
||||||
@Override
|
switch (msg.what) {
|
||||||
public void handleMessage(final Message msg) {
|
case HANDLER_OPEN_IME_KEYBOARD: {
|
||||||
switch (msg.what) {
|
GodotEditText edit = (GodotEditText)msg.obj;
|
||||||
case HANDLER_OPEN_IME_KEYBOARD: {
|
String text = edit.mOriginText;
|
||||||
GodotEditText edit = (GodotEditText)msg.obj;
|
if (edit.requestFocus()) {
|
||||||
String text = edit.mOriginText;
|
edit.removeTextChangedListener(edit.mInputWrapper);
|
||||||
if (edit.requestFocus()) {
|
edit.setText("");
|
||||||
edit.removeTextChangedListener(edit.mInputWrapper);
|
edit.append(text);
|
||||||
edit.setText("");
|
edit.mInputWrapper.setOriginText(text);
|
||||||
edit.append(text);
|
edit.addTextChangedListener(edit.mInputWrapper);
|
||||||
edit.mInputWrapper.setOriginText(text);
|
final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
edit.addTextChangedListener(edit.mInputWrapper);
|
imm.showSoftInput(edit, 0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
} break;
|
||||||
};
|
|
||||||
|
case HANDLER_CLOSE_IME_KEYBOARD: {
|
||||||
|
GodotEditText edit = (GodotEditText)msg.obj;
|
||||||
|
|
||||||
|
edit.removeTextChangedListener(mInputWrapper);
|
||||||
|
final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
|
||||||
|
edit.mView.requestFocus();
|
||||||
|
} break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package org.godotengine.godot.input;
|
package org.godotengine.godot.input;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@ -130,11 +129,7 @@ public interface InputManagerCompat {
|
|||||||
* @return a compatible implementation of InputManager
|
* @return a compatible implementation of InputManager
|
||||||
*/
|
*/
|
||||||
public static InputManagerCompat getInputManager(Context context) {
|
public static InputManagerCompat getInputManager(Context context) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
return new InputManagerV16(context);
|
||||||
return new InputManagerV16(context);
|
|
||||||
} else {
|
|
||||||
return new InputManagerV9();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,209 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.godotengine.godot.input;
|
|
||||||
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.SparseArray;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Queue;
|
|
||||||
|
|
||||||
public class InputManagerV9 implements InputManagerCompat {
|
|
||||||
private static final String LOG_TAG = "InputManagerV9";
|
|
||||||
private static final int MESSAGE_TEST_FOR_DISCONNECT = 101;
|
|
||||||
private static final long CHECK_ELAPSED_TIME = 3000L;
|
|
||||||
|
|
||||||
private static final int ON_DEVICE_ADDED = 0;
|
|
||||||
private static final int ON_DEVICE_CHANGED = 1;
|
|
||||||
private static final int ON_DEVICE_REMOVED = 2;
|
|
||||||
|
|
||||||
private final SparseArray<long[]> mDevices;
|
|
||||||
private final Map<InputDeviceListener, Handler> mListeners;
|
|
||||||
private final Handler mDefaultHandler;
|
|
||||||
|
|
||||||
private static class PollingMessageHandler extends Handler {
|
|
||||||
private final WeakReference<InputManagerV9> mInputManager;
|
|
||||||
|
|
||||||
PollingMessageHandler(InputManagerV9 im) {
|
|
||||||
mInputManager = new WeakReference<InputManagerV9>(im);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
super.handleMessage(msg);
|
|
||||||
switch (msg.what) {
|
|
||||||
case MESSAGE_TEST_FOR_DISCONNECT:
|
|
||||||
InputManagerV9 imv = mInputManager.get();
|
|
||||||
if (null != imv) {
|
|
||||||
long time = SystemClock.elapsedRealtime();
|
|
||||||
int size = imv.mDevices.size();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
long[] lastContact = imv.mDevices.valueAt(i);
|
|
||||||
if (null != lastContact) {
|
|
||||||
if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
|
|
||||||
// check to see if the device has been
|
|
||||||
// disconnected
|
|
||||||
int id = imv.mDevices.keyAt(i);
|
|
||||||
if (null == InputDevice.getDevice(id)) {
|
|
||||||
// disconnected!
|
|
||||||
imv.notifyListeners(ON_DEVICE_REMOVED, id);
|
|
||||||
imv.mDevices.remove(id);
|
|
||||||
} else {
|
|
||||||
lastContact[0] = time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
|
|
||||||
CHECK_ELAPSED_TIME);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputManagerV9() {
|
|
||||||
mDevices = new SparseArray<long[]>();
|
|
||||||
mListeners = new HashMap<InputDeviceListener, Handler>();
|
|
||||||
mDefaultHandler = new PollingMessageHandler(this);
|
|
||||||
// as a side-effect, populates our collection of watched
|
|
||||||
// input devices
|
|
||||||
getInputDeviceIds();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputDevice getInputDevice(int id) {
|
|
||||||
return InputDevice.getDevice(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int[] getInputDeviceIds() {
|
|
||||||
// add any hitherto unknown devices to our
|
|
||||||
// collection of watched input devices
|
|
||||||
int[] activeDevices = InputDevice.getDeviceIds();
|
|
||||||
long time = SystemClock.elapsedRealtime();
|
|
||||||
for (int id : activeDevices) {
|
|
||||||
long[] lastContact = mDevices.get(id);
|
|
||||||
if (null == lastContact) {
|
|
||||||
// we have a new device
|
|
||||||
mDevices.put(id, new long[] { time });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return activeDevices;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
|
|
||||||
mListeners.remove(listener);
|
|
||||||
if (handler == null) {
|
|
||||||
handler = mDefaultHandler;
|
|
||||||
}
|
|
||||||
mListeners.put(listener, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unregisterInputDeviceListener(InputDeviceListener listener) {
|
|
||||||
mListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyListeners(int why, int deviceId) {
|
|
||||||
// the state of some device has changed
|
|
||||||
if (!mListeners.isEmpty()) {
|
|
||||||
// yes... this will cause an object to get created... hopefully
|
|
||||||
// it won't happen very often
|
|
||||||
for (InputDeviceListener listener : mListeners.keySet()) {
|
|
||||||
Handler handler = mListeners.get(listener);
|
|
||||||
DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener);
|
|
||||||
handler.post(odc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DeviceEvent implements Runnable {
|
|
||||||
private int mMessageType;
|
|
||||||
private int mId;
|
|
||||||
private InputDeviceListener mListener;
|
|
||||||
private static Queue<DeviceEvent> sEventQueue = new ArrayDeque<DeviceEvent>();
|
|
||||||
|
|
||||||
private DeviceEvent() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static DeviceEvent getDeviceEvent(int messageType, int id,
|
|
||||||
InputDeviceListener listener) {
|
|
||||||
DeviceEvent curChanged = sEventQueue.poll();
|
|
||||||
if (null == curChanged) {
|
|
||||||
curChanged = new DeviceEvent();
|
|
||||||
}
|
|
||||||
curChanged.mMessageType = messageType;
|
|
||||||
curChanged.mId = id;
|
|
||||||
curChanged.mListener = listener;
|
|
||||||
return curChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
switch (mMessageType) {
|
|
||||||
case ON_DEVICE_ADDED:
|
|
||||||
mListener.onInputDeviceAdded(mId);
|
|
||||||
break;
|
|
||||||
case ON_DEVICE_CHANGED:
|
|
||||||
mListener.onInputDeviceChanged(mId);
|
|
||||||
break;
|
|
||||||
case ON_DEVICE_REMOVED:
|
|
||||||
mListener.onInputDeviceRemoved(mId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.e(LOG_TAG, "Unknown Message Type");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// dump this runnable back in the queue
|
|
||||||
sEventQueue.offer(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onGenericMotionEvent(MotionEvent event) {
|
|
||||||
// detect new devices
|
|
||||||
int id = event.getDeviceId();
|
|
||||||
long[] timeArray = mDevices.get(id);
|
|
||||||
if (null == timeArray) {
|
|
||||||
notifyListeners(ON_DEVICE_ADDED, id);
|
|
||||||
timeArray = new long[1];
|
|
||||||
mDevices.put(id, timeArray);
|
|
||||||
}
|
|
||||||
long time = SystemClock.elapsedRealtime();
|
|
||||||
timeArray[0] = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
mDefaultHandler.sendEmptyMessage(MESSAGE_TEST_FOR_DISCONNECT);
|
|
||||||
}
|
|
||||||
}
|
|
@ -37,59 +37,81 @@ import android.os.AsyncTask;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
abstract public class ConsumeTask {
|
abstract public class ConsumeTask {
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
private IInAppBillingService mService;
|
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) {
|
public ConsumeTask(IInAppBillingService mService, Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mService = mService;
|
this.mService = mService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void consume(final String sku) {
|
public void consume(final String sku) {
|
||||||
//Log.d("XXX", "Consuming product " + sku);
|
mSku = sku;
|
||||||
PaymentsCache pc = new PaymentsCache(context);
|
PaymentsCache pc = new PaymentsCache(context);
|
||||||
Boolean isBlocked = pc.getConsumableFlag("block", sku);
|
Boolean isBlocked = pc.getConsumableFlag("block", sku);
|
||||||
String _token = pc.getConsumableValue("token", sku);
|
mToken = pc.getConsumableValue("token", sku);
|
||||||
//Log.d("XXX", "token " + _token);
|
if (!isBlocked && mToken == null) {
|
||||||
if (!isBlocked && _token == null) {
|
// Consuming task is processing
|
||||||
//_token = "inapp:"+context.getPackageName()+":android.test.purchased";
|
|
||||||
//Log.d("XXX", "Consuming product " + sku + " with token " + _token);
|
|
||||||
} else if (!isBlocked) {
|
} else if (!isBlocked) {
|
||||||
//Log.d("XXX", "It is not blocked ¿?");
|
|
||||||
return;
|
return;
|
||||||
} else if (_token == null) {
|
} else if (mToken == null) {
|
||||||
//Log.d("XXX", "No token available");
|
|
||||||
this.error("No token for sku:" + sku);
|
this.error("No token for sku:" + sku);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final String token = _token;
|
new ConsumeAsyncTask(this).execute();
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onPostExecute(String param) {
|
private String doInBackground(String... params) {
|
||||||
if (param == null) {
|
try {
|
||||||
success(new PaymentsCache(context).getConsumableValue("ticket", sku));
|
int response = mService.consumePurchase(3, context.getPackageName(), mToken);
|
||||||
} else {
|
if (response == 0 || response == 8) {
|
||||||
error(param);
|
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);
|
abstract protected void success(String ticket);
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
/*************************************************************************/
|
|
||||||
/* GenericConsumeTask.java */
|
|
||||||
/*************************************************************************/
|
|
||||||
/* This file is part of: */
|
|
||||||
/* GODOT ENGINE */
|
|
||||||
/* https://godotengine.org */
|
|
||||||
/*************************************************************************/
|
|
||||||
/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */
|
|
||||||
/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */
|
|
||||||
/* */
|
|
||||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
||||||
/* a copy of this software and associated documentation files (the */
|
|
||||||
/* "Software"), to deal in the Software without restriction, including */
|
|
||||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
||||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
||||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
||||||
/* the following conditions: */
|
|
||||||
/* */
|
|
||||||
/* The above copyright notice and this permission notice shall be */
|
|
||||||
/* included in all copies or substantial portions of the Software. */
|
|
||||||
/* */
|
|
||||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
||||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
||||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
|
||||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
||||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
||||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
||||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
||||||
/*************************************************************************/
|
|
||||||
|
|
||||||
package org.godotengine.godot.payments;
|
|
||||||
|
|
||||||
import com.android.vending.billing.IInAppBillingService;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
abstract public class GenericConsumeTask extends AsyncTask<String, String, String> {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private IInAppBillingService mService;
|
|
||||||
|
|
||||||
public GenericConsumeTask(Context context, IInAppBillingService mService, String sku, String receipt, String signature, String token) {
|
|
||||||
this.context = context;
|
|
||||||
this.mService = mService;
|
|
||||||
this.sku = sku;
|
|
||||||
this.receipt = receipt;
|
|
||||||
this.signature = signature;
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sku;
|
|
||||||
private String receipt;
|
|
||||||
private String signature;
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(String... params) {
|
|
||||||
try {
|
|
||||||
//Log.d("godot", "Requesting to consume an item with token ." + token);
|
|
||||||
int response = mService.consumePurchase(3, context.getPackageName(), token);
|
|
||||||
//Log.d("godot", "consumePurchase response: " + response);
|
|
||||||
if (response == 0 || response == 8) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onPostExecute(String sarasa) {
|
|
||||||
onSuccess(sku, receipt, signature, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract public void onSuccess(String sku, String receipt, String signature, String token);
|
|
||||||
}
|
|
@ -46,7 +46,7 @@ public class PaymentsCache {
|
|||||||
SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE);
|
SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = sharedPref.edit();
|
SharedPreferences.Editor editor = sharedPref.edit();
|
||||||
editor.putBoolean(sku, flag);
|
editor.putBoolean(sku, flag);
|
||||||
editor.commit();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getConsumableFlag(String set, String sku) {
|
public boolean getConsumableFlag(String set, String sku) {
|
||||||
@ -60,7 +60,7 @@ public class PaymentsCache {
|
|||||||
SharedPreferences.Editor editor = sharedPref.edit();
|
SharedPreferences.Editor editor = sharedPref.edit();
|
||||||
editor.putString(sku, value);
|
editor.putString(sku, value);
|
||||||
//Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku);
|
//Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku);
|
||||||
editor.commit();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getConsumableValue(String set, String sku) {
|
public String getConsumableValue(String set, String sku) {
|
||||||
|
@ -112,7 +112,7 @@ public class PaymentsManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public void requestPurchase(final String sku, String transactionId) {
|
public void requestPurchase(final String sku, String transactionId) {
|
||||||
new PurchaseTask(mService, Godot.getInstance()) {
|
new PurchaseTask(mService, activity) {
|
||||||
@Override
|
@Override
|
||||||
protected void error(String message) {
|
protected void error(String message) {
|
||||||
godotPaymentV3.callbackFail(message);
|
godotPaymentV3.callbackFail(message);
|
||||||
@ -159,7 +159,7 @@ public class PaymentsManager {
|
|||||||
|
|
||||||
public void requestPurchased() {
|
public void requestPurchased() {
|
||||||
try {
|
try {
|
||||||
PaymentsCache pc = new PaymentsCache(Godot.getInstance());
|
PaymentsCache pc = new PaymentsCache(activity);
|
||||||
|
|
||||||
String continueToken = null;
|
String continueToken = null;
|
||||||
|
|
||||||
|
@ -30,26 +30,59 @@
|
|||||||
|
|
||||||
package org.godotengine.godot.payments;
|
package org.godotengine.godot.payments;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.vending.billing.IInAppBillingService;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import org.godotengine.godot.Dictionary;
|
import java.lang.ref.WeakReference;
|
||||||
import org.godotengine.godot.Godot;
|
import java.util.ArrayList;
|
||||||
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;
|
|
||||||
|
|
||||||
abstract public class ReleaseAllConsumablesTask {
|
abstract public class ReleaseAllConsumablesTask {
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private IInAppBillingService mService;
|
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) {
|
public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mService = mService;
|
this.mService = mService;
|
||||||
@ -60,12 +93,6 @@ abstract public class ReleaseAllConsumablesTask {
|
|||||||
//Log.d("godot", "consumeItall for " + context.getPackageName());
|
//Log.d("godot", "consumeItall for " + context.getPackageName());
|
||||||
Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null);
|
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) {
|
if (bundle.getInt("RESPONSE_CODE") == 0) {
|
||||||
|
|
||||||
final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
|
final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
|
||||||
@ -87,14 +114,7 @@ abstract public class ReleaseAllConsumablesTask {
|
|||||||
String token = inappPurchaseData.getString("purchaseToken");
|
String token = inappPurchaseData.getString("purchaseToken");
|
||||||
String signature = mySignatures.get(i);
|
String signature = mySignatures.get(i);
|
||||||
//Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt);
|
//Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt);
|
||||||
new GenericConsumeTask(context, mService, sku, receipt, signature, token) {
|
new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute();
|
||||||
@Override
|
|
||||||
public void onSuccess(String sku, String receipt, String signature, String token) {
|
|
||||||
ReleaseAllConsumablesTask.this.success(sku, receipt, signature, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
} catch (JSONException e) {
|
} 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 success(String sku, String receipt, String signature, String token);
|
||||||
abstract protected void error(String message);
|
abstract protected void error(String message);
|
||||||
abstract protected void notRequired();
|
abstract protected void notRequired();
|
||||||
|
@ -52,69 +52,104 @@ import android.os.Bundle;
|
|||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
abstract public class ValidateTask {
|
abstract public class ValidateTask {
|
||||||
|
|
||||||
private Activity context;
|
private Activity context;
|
||||||
private GodotPaymentV3 godotPaymentsV3;
|
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) {
|
public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.godotPaymentsV3 = godotPaymentsV3;
|
this.godotPaymentsV3 = godotPaymentsV3;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validatePurchase(final String sku) {
|
public void validatePurchase(final String sku) {
|
||||||
new AsyncTask<String, String, String>() {
|
mSku = sku;
|
||||||
private ProgressDialog dialog;
|
new ValidateAsyncTask(this).execute();
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
dialog = ProgressDialog.show(context, null, "Please wait...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(String... params) {
|
|
||||||
PaymentsCache pc = new PaymentsCache(context);
|
|
||||||
String url = godotPaymentsV3.getPurchaseValidationUrlPrefix();
|
|
||||||
RequestParams param = new RequestParams();
|
|
||||||
param.setUrl(url);
|
|
||||||
param.put("ticket", pc.getConsumableValue("ticket", sku));
|
|
||||||
param.put("purchaseToken", pc.getConsumableValue("token", sku));
|
|
||||||
param.put("sku", sku);
|
|
||||||
//Log.d("XXX", "Haciendo request a " + url);
|
|
||||||
//Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku));
|
|
||||||
//Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku));
|
|
||||||
//Log.d("XXX", "sku: " + sku);
|
|
||||||
param.put("package", context.getApplicationContext().getPackageName());
|
|
||||||
HttpRequester requester = new HttpRequester();
|
|
||||||
String jsonResponse = requester.post(param);
|
|
||||||
//Log.d("XXX", "Validation response:\n"+jsonResponse);
|
|
||||||
return jsonResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(String response) {
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
JSONObject j;
|
|
||||||
try {
|
|
||||||
j = new JSONObject(response);
|
|
||||||
if (j.getString("status").equals("OK")) {
|
|
||||||
success();
|
|
||||||
return;
|
|
||||||
} else if (j.getString("status") != null) {
|
|
||||||
error(j.getString("message"));
|
|
||||||
} else {
|
|
||||||
error("Connection error");
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
error(e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onPreExecute() {
|
||||||
|
dialog = ProgressDialog.show(context, null, "Please wait...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String doInBackground(String... params) {
|
||||||
|
PaymentsCache pc = new PaymentsCache(context);
|
||||||
|
String url = godotPaymentsV3.getPurchaseValidationUrlPrefix();
|
||||||
|
RequestParams param = new RequestParams();
|
||||||
|
param.setUrl(url);
|
||||||
|
param.put("ticket", pc.getConsumableValue("ticket", mSku));
|
||||||
|
param.put("purchaseToken", pc.getConsumableValue("token", mSku));
|
||||||
|
param.put("sku", mSku);
|
||||||
|
//Log.d("XXX", "Haciendo request a " + url);
|
||||||
|
//Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku));
|
||||||
|
//Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku));
|
||||||
|
//Log.d("XXX", "sku: " + sku);
|
||||||
|
param.put("package", context.getApplicationContext().getPackageName());
|
||||||
|
HttpRequester requester = new HttpRequester();
|
||||||
|
String jsonResponse = requester.post(param);
|
||||||
|
//Log.d("XXX", "Validation response:\n"+jsonResponse);
|
||||||
|
return jsonResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPostExecute(String response) {
|
||||||
|
if (dialog != null) {
|
||||||
|
dialog.dismiss();
|
||||||
|
dialog = null;
|
||||||
|
}
|
||||||
|
JSONObject j;
|
||||||
|
try {
|
||||||
|
j = new JSONObject(response);
|
||||||
|
if (j.getString("status").equals("OK")) {
|
||||||
|
success();
|
||||||
|
return;
|
||||||
|
} else if (j.getString("status") != null) {
|
||||||
|
error(j.getString("message"));
|
||||||
|
} else {
|
||||||
|
error("Connection error");
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract protected void success();
|
abstract protected void success();
|
||||||
abstract protected void error(String message);
|
abstract protected void error(String message);
|
||||||
abstract protected void canceled();
|
abstract protected void canceled();
|
||||||
|
@ -105,7 +105,7 @@ public class HttpRequester {
|
|||||||
long timeInit = new Date().getTime();
|
long timeInit = new Date().getTime();
|
||||||
response = request(httpget);
|
response = request(httpget);
|
||||||
long delay = new Date().getTime() - timeInit;
|
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) {
|
if (response == null || response.length() == 0) {
|
||||||
response = "";
|
response = "";
|
||||||
} else {
|
} else {
|
||||||
@ -200,7 +200,7 @@ public class HttpRequester {
|
|||||||
SharedPreferences.Editor editor = sharedPref.edit();
|
SharedPreferences.Editor editor = sharedPref.edit();
|
||||||
editor.putString("request_" + Crypt.md5(request), response);
|
editor.putString("request_" + Crypt.md5(request), response);
|
||||||
editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl());
|
editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl());
|
||||||
editor.commit();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getResponseFromCache(String request) {
|
public String getResponseFromCache(String request) {
|
||||||
|