diff options
Diffstat (limited to 'platform/android/java')
163 files changed, 8372 insertions, 7417 deletions
diff --git a/platform/android/java/AndroidManifest.xml b/platform/android/java/AndroidManifest.xml deleted file mode 100644 index 3152ef12cd..0000000000 --- a/platform/android/java/AndroidManifest.xml +++ /dev/null @@ -1,58 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.godot.game" - android:versionCode="1" - android:versionName="1.0" - android:installLocation="auto" - > -<supports-screens android:smallScreens="true" - android:normalScreens="true" - android:largeScreens="true" - android:xlargeScreens="true"/> - -<!--glEsVersion is modified by the exporter, changing this value here has no effect--> - <uses-feature android:glEsVersion="0x00020000" android:required="true" /> -<!--Adding custom text to manifest is fine, but do it outside the custom user and application BEGIN/END regions, as that gets rewritten--> - -<!--Custom permissions XML added by add-ons. It's recommended to add them from the export preset, though--> -<!--CHUNK_USER_PERMISSIONS_BEGIN--> -<!--CHUNK_USER_PERMISSIONS_END--> - -<!--Anything in this line after the icon will be erased when doing custom build. If you want to add tags manually, do before it.--> - <application android:label="@string/godot_project_name_string" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" android:icon="@drawable/icon"> - -<!--The following values are replaced when Godot exports, modifying them here has no effect. Do these changes in the--> -<!--export preset. Adding new ones is fine.--> - -<!-- XR mode metadata. This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> - <meta-data android:name="xr_mode_metadata_name" android:value="xr_mode_metadata_value"/> - - <activity android:name="org.godotengine.godot.Godot" - android:label="@string/godot_project_name_string" - android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" - android:launchMode="singleTask" - android:screenOrientation="landscape" - android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:resizeableActivity="false" - tools:ignore="UnusedAttribute"> - - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - </activity> - <service android:name="org.godotengine.godot.GodotDownloaderService" /> - -<!--Custom application XML added by add-ons--> -<!--CHUNK_APPLICATION_BEGIN--> -<!--CHUNK_APPLICATION_END--> - - </application> - - <instrumentation android:icon="@drawable/icon" - android:label="@string/godot_project_name_string" - android:name="org.godotengine.godot.GodotInstrumentation" - android:targetPackage="org.godotengine.game" /> - -</manifest> diff --git a/platform/android/java/README.md b/platform/android/java/README.md deleted file mode 100644 index 58d2b10706..0000000000 --- a/platform/android/java/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Third party libraries - - -## Google's vending library - -- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/src/main/java/com/google/android/vending -- Version: git (eb57657, 2018) with modifications -- License: Apache 2.0 - -Overwrite all files under `com/google/android/vending` - -### Modify some files to avoid compile error and lint warning - -#### com/google/android/vending/licensing/util/Base64.java -``` -@@ -338,7 +338,8 @@ public class Base64 { - e += 4; - } - -- assert (e == outBuff.length); -+ if (BuildConfig.DEBUG && e != outBuff.length) -+ throw new RuntimeException(); - return outBuff; - } -``` - -#### com/google/android/vending/licensing/LicenseChecker.java -``` -@@ -29,8 +29,8 @@ import android.os.RemoteException; - import android.provider.Settings.Secure; - import android.util.Log; - --import com.android.vending.licensing.ILicenseResultListener; --import com.android.vending.licensing.ILicensingService; -+import com.google.android.vending.licensing.ILicenseResultListener; -+import com.google.android.vending.licensing.ILicensingService; - import com.google.android.vending.licensing.util.Base64; - import com.google.android.vending.licensing.util.Base64DecoderException; -``` -``` -@@ -287,13 +287,15 @@ public class LicenseChecker implements ServiceConnection { - if (logResponse) { -- String android_id = Secure.getString(mContext.getContentResolver(), -- Secure.ANDROID_ID); -+ String android_id = Secure.ANDROID_ID; - Date date = new Date(); -``` diff --git a/platform/android/java/aidl/com/android/vending/billing/IInAppBillingService.aidl b/platform/android/java/aidl/com/android/vending/billing/IInAppBillingService.aidl deleted file mode 100644 index 2a492f7845..0000000000 --- a/platform/android/java/aidl/com/android/vending/billing/IInAppBillingService.aidl +++ /dev/null @@ -1,144 +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.android.vending.billing; - -import android.os.Bundle; - -/** - * InAppBillingService is the service that provides in-app billing version 3 and beyond. - * This service provides the following features: - * 1. Provides a new API to get details of in-app items published for the app including - * price, type, title and description. - * 2. The purchase flow is synchronous and purchase information is available immediately - * after it completes. - * 3. Purchase information of in-app purchases is maintained within the Google Play system - * till the purchase is consumed. - * 4. An API to consume a purchase of an inapp item. All purchases of one-time - * in-app items are consumable and thereafter can be purchased again. - * 5. An API to get current purchases of the user immediately. This will not contain any - * consumed purchases. - * - * All calls will give a response code with the following possible values - * RESULT_OK = 0 - success - * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog - * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested - * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase - * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API - * RESULT_ERROR = 6 - Fatal error during the API action - * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned - * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned - */ -interface IInAppBillingService { - /** - * Checks support for the requested billing API version, package and in-app type. - * Minimum API version supported by this interface is 3. - * @param apiVersion the billing version which the app is using - * @param packageName the package name of the calling app - * @param type type of the in-app item being purchased "inapp" for one-time purchases - * and "subs" for subscription. - * @return RESULT_OK(0) on success, corresponding result code on failures - */ - int isBillingSupported(int apiVersion, String packageName, String type); - - /** - * Provides details of a list of SKUs - * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle - * with a list JSON strings containing the productId, price, title and description. - * This API can be called with a maximum of 20 SKUs. - * @param apiVersion billing API version that the Third-party is using - * @param packageName the package name of the calling app - * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "DETAILS_LIST" with a StringArrayList containing purchase information - * in JSON format similar to: - * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", - * "title : "Example Title", "description" : "This is an example description" }' - */ - Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); - - /** - * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, - * the type, a unique purchase token and an optional developer payload. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param sku the SKU of the in-app item as published in the developer console - * @param type the type of the in-app item ("inapp" for one-time purchases - * and "subs" for subscription). - * @param developerPayload optional argument to be sent back with the purchase information - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "BUY_INTENT" - PendingIntent to start the purchase flow - * - * The Pending intent should be launched with startIntentSenderForResult. When purchase flow - * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. - * If the purchase is successful, the result data will contain the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "INAPP_PURCHASE_DATA" - String in JSON format similar to - * '{"orderId":"12999763169054705758.1371079406387615", - * "packageName":"com.example.app", - * "productId":"exampleSku", - * "purchaseTime":1345678900000, - * "purchaseToken" : "122333444455555", - * "developerPayload":"example developer payload" }' - * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that - * was signed with the private key of the developer - * TODO: change this to app-specific keys. - */ - Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, - String developerPayload); - - /** - * Returns the current SKUs owned by the user of the type and package name specified along with - * purchase information and a signature of the data to be validated. - * This will return all SKUs that have been purchased in V3 and managed items purchased using - * V1 and V2 that have not been consumed. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param type the type of the in-app items being requested - * ("inapp" for one-time purchases and "subs" for subscription). - * @param continuationToken to be set as null for the first call, if the number of owned - * skus are too many, a continuationToken is returned in the response bundle. - * This method can be called again with the continuation token to get the next set of - * owned skus. - * @return Bundle containing the following key-value pairs - * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on - * failure as listed above. - * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs - * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information - * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures - * of the purchase information - * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the - * next set of in-app purchases. Only set if the - * user has more owned skus than the current list. - */ - Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); - - /** - * Consume the last purchase of the given SKU. This will result in this item being removed - * from all subsequent responses to getPurchases() and allow re-purchase of this item. - * @param apiVersion billing API version that the app is using - * @param packageName package name of the calling app - * @param purchaseToken token in the purchase information JSON that identifies the purchase - * to be consumed - * @return 0 if consumption succeeded. Appropriate error values for failures. - */ - int consumePurchase(int apiVersion, String packageName, String purchaseToken); -} diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml new file mode 100644 index 0000000000..ba01ec313b --- /dev/null +++ b/platform/android/java/app/AndroidManifest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.godot.game" + android:versionCode="1" + android:versionName="1.0" + android:installLocation="auto" > + + <!-- Adding custom text to the manifest is fine, but do it outside the custom USER and APPLICATION BEGIN/END comments, --> + <!-- as that gets rewritten. --> + + <supports-screens + android:smallScreens="true" + android:normalScreens="true" + android:largeScreens="true" + android:xlargeScreens="true" /> + + <!-- glEsVersion is modified by the exporter, changing this value here has no effect. --> + <uses-feature + android:glEsVersion="0x00020000" + android:required="true" /> + +<!-- Custom user permissions XML added by add-ons. It's recommended to add them from the export preset, though. --> +<!--CHUNK_USER_PERMISSIONS_BEGIN--> +<!--CHUNK_USER_PERMISSIONS_END--> + + <!-- Any tag in this line after android:icon will be erased when doing custom builds. --> + <!-- If you want to add tags manually, do before it. --> + <!-- WARNING: This should stay on a single line until the parsing code is improved. See GH-32414. --> + <application android:label="@string/godot_project_name_string" android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning" android:icon="@drawable/icon" > + + <!-- The following metadata values are replaced when Godot exports, modifying them here has no effect. --> + <!-- Do these changes in the export preset. Adding new ones is fine. --> + + <!-- XR mode metadata. This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> + <meta-data + android:name="xr_mode_metadata_name" + android:value="xr_mode_metadata_value" /> + + <activity + android:name=".GodotApp" + android:label="@string/godot_project_name_string" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:launchMode="singleTask" + android:screenOrientation="landscape" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:resizeableActivity="false" + tools:ignore="UnusedAttribute" > + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + +<!-- Custom application XML added by add-ons. --> +<!--CHUNK_APPLICATION_BEGIN--> +<!--CHUNK_APPLICATION_END--> + + </application> + +</manifest> diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle new file mode 100644 index 0000000000..9f64c3dc8a --- /dev/null +++ b/platform/android/java/app/build.gradle @@ -0,0 +1,121 @@ +// Gradle build config for Godot Engine's Android port. +// +// Do not remove/modify comments ending with BEGIN/END, they are used to inject +// addon-specific configuration. +apply from: 'config.gradle' + +buildscript { + apply from: 'config.gradle' + + repositories { + google() + jcenter() +//CHUNK_BUILDSCRIPT_REPOSITORIES_BEGIN +//CHUNK_BUILDSCRIPT_REPOSITORIES_END + } + dependencies { + classpath libraries.androidGradlePlugin +//CHUNK_BUILDSCRIPT_DEPENDENCIES_BEGIN +//CHUNK_BUILDSCRIPT_DEPENDENCIES_END + } +} + +apply plugin: 'com.android.application' + +allprojects { + repositories { + mavenCentral() + google() + jcenter() +//CHUNK_ALLPROJECTS_REPOSITORIES_BEGIN +//CHUNK_ALLPROJECTS_REPOSITORIES_END + } +} + +dependencies { + if (rootProject.findProject(":lib")) { + implementation project(":lib") + } else { + // Custom build mode. In this scenario this project is the only one around and the Godot + // library is available through the pre-generated godot-lib.*.aar android archive files. + debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) + releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) + } +//CHUNK_DEPENDENCIES_BEGIN +//CHUNK_DEPENDENCIES_END +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + // Feel free to modify the application id to your own. + applicationId "com.godot.game" + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk +//CHUNK_ANDROID_DEFAULTCONFIG_BEGIN +//CHUNK_ANDROID_DEFAULTCONFIG_END + } + + lintOptions { + abortOnError false + disable 'MissingTranslation', 'UnusedResources' + } + + packagingOptions { + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + } + + // Both signing and zip-aligning will be done at export time + buildTypes.all { buildType -> + buildType.zipAlignEnabled false + buildType.signingConfig null + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [ + 'src' +//DIR_SRC_BEGIN +//DIR_SRC_END + ] + res.srcDirs = [ + 'res' +//DIR_RES_BEGIN +//DIR_RES_END + ] + aidl.srcDirs = [ + 'aidl' +//DIR_AIDL_BEGIN +//DIR_AIDL_END + ] + assets.srcDirs = [ + 'assets' +//DIR_ASSETS_BEGIN +//DIR_ASSETS_END + ] + } + debug.jniLibs.srcDirs = [ + 'libs/debug' +//DIR_JNI_DEBUG_BEGIN +//DIR_JNI_DEBUG_END + ] + release.jniLibs.srcDirs = [ + 'libs/release' +//DIR_JNI_RELEASE_BEGIN +//DIR_JNI_RELEASE_END + ] + } + + applicationVariants.all { variant -> + variant.outputs.all { output -> + output.outputFileName = "android_${variant.name}.apk" + } + } +} + +//CHUNK_GLOBAL_BEGIN +//CHUNK_GLOBAL_END diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle new file mode 100644 index 0000000000..20c3123221 --- /dev/null +++ b/platform/android/java/app/config.gradle @@ -0,0 +1,12 @@ +ext.versions = [ + androidGradlePlugin : '3.4.2', + compileSdk : 28, + minSdk : 18, + targetSdk : 28, + buildTools : '28.0.3', + +] + +ext.libraries = [ + androidGradlePlugin : "com.android.tools.build:gradle:$versions.androidGradlePlugin" +] diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java new file mode 100644 index 0000000000..d7469a8765 --- /dev/null +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* GodotApp.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2019 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 com.godot.game; + +import org.godotengine.godot.Godot; + +/** + * Template activity for Godot Android custom builds. + * Feel free to extend and modify this class for your custom logic. + */ +public class GodotApp extends Godot { +} diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index c468277daa..2052017888 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -1,113 +1,152 @@ -//Gradle project for Godot Engine Android port. -//Do not modify code between the BEGIN/END sections, as it's autogenerated by add-ons +apply from: 'app/config.gradle' buildscript { - repositories { - google() - jcenter() -//CHUNK_BUILDSCRIPT_REPOSITORIES_BEGIN -//CHUNK_BUILDSCRIPT_REPOSITORIES_END - } - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' -//CHUNK_BUILDSCRIPT_DEPENDENCIES_BEGIN -//CHUNK_BUILDSCRIPT_DEPENDENCIES_END - } -} + apply from: 'app/config.gradle' -apply plugin: 'com.android.application' + repositories { + google() + jcenter() + } + dependencies { + classpath libraries.androidGradlePlugin + } +} allprojects { repositories { - mavenCentral() - google() - jcenter() -//CHUNK_ALLPROJECTS_REPOSITORIES_BEGIN -//CHUNK_ALLPROJECTS_REPOSITORIES_END - + google() + jcenter() + mavenCentral() } } -dependencies { - implementation "com.android.support:support-core-utils:28.0.0" -//CHUNK_DEPENDENCIES_BEGIN -//CHUNK_DEPENDENCIES_END +ext { + sconsExt = org.gradle.internal.os.OperatingSystem.current().isWindows() ? ".bat" : "" + + supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] + supportedTargets = ['release':"release", 'debug':"release_debug"] + + // Used by gradle to specify which architecture to build for by default when running `./gradlew build`. + // This command is usually used by Android Studio. + // If building manually on the command line, it's recommended to use the + // `./gradlew generateGodotTemplates` build command instead after running the `scons` command. + // The defaultAbi must be one of the {supportedAbis} values. + defaultAbi = "arm64v8" } -android { - - lintOptions { - abortOnError false - disable 'MissingTranslation','UnusedResources' - } - - compileSdkVersion 28 - buildToolsVersion "28.0.3" - useLibrary 'org.apache.http.legacy' - - packagingOptions { - exclude 'META-INF/LICENSE' - exclude 'META-INF/NOTICE' - } - defaultConfig { - minSdkVersion 18 - targetSdkVersion 28 -//CHUNK_ANDROID_DEFAULTCONFIG_BEGIN -//CHUNK_ANDROID_DEFAULTCONFIG_END - } - // Both signing and zip-aligning will be done at export time - buildTypes.all { buildType -> - buildType.zipAlignEnabled false - buildType.signingConfig null - } - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src' -//DIR_SRC_BEGIN -//DIR_SRC_END - ] - res.srcDirs = [ - 'res' -//DIR_RES_BEGIN -//DIR_RES_END - ] - aidl.srcDirs = [ - 'aidl' -//DIR_AIDL_BEGIN -//DIR_AIDL_END - ] - assets.srcDirs = [ - 'assets' -//DIR_ASSETS_BEGIN -//DIR_ASSETS_END - - ] - } - debug.jniLibs.srcDirs = [ - 'libs/debug' -//DIR_JNI_DEBUG_BEGIN -//DIR_JNI_DEBUG_END - ] - release.jniLibs.srcDirs = [ - 'libs/release' -//DIR_JNI_RELEASE_BEGIN -//DIR_JNI_RELEASE_END - ] - } -// No longer used, as it's not useful for build source template -// applicationVariants.all { variant -> -// variant.outputs.all { output -> -// output.outputFileName = "../../../../../../../bin/android_${variant.name}.apk" -// } -// } +def rootDir = "../../.." +def binDir = "$rootDir/bin/" +def getSconsTaskName(String buildType) { + return "compileGodotNativeLibs" + buildType.capitalize() } -//CHUNK_GLOBAL_BEGIN -//CHUNK_GLOBAL_END +/** + * Copy the generated 'android_debug.apk' binary template into the Godot bin directory. + * Depends on the app build task to ensure the binary is generated prior to copying. + */ +task copyDebugBinaryToBin(type: Copy) { + dependsOn ':app:assembleDebug' + from('app/build/outputs/apk/debug') + into(binDir) + include('android_debug.apk') +} +/** + * Copy the generated 'android_release.apk' binary template into the Godot bin directory. + * Depends on the app build task to ensure the binary is generated prior to copying. + */ +task copyReleaseBinaryToBin(type: Copy) { + dependsOn ':app:assembleRelease' + from('app/build/outputs/apk/release') + into(binDir) + include('android_release.apk') +} +/** + * Copy the Godot android library archive debug file into the app debug libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDebugAAR(type: Copy) { + dependsOn ':lib:assembleDebug' + from('lib/build/outputs/aar') + into('app/libs/debug') + include('godot-lib.debug.aar') +} +/** + * Copy the Godot android library archive release file into the app release libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyReleaseAAR(type: Copy) { + dependsOn ':lib:assembleRelease' + from('lib/build/outputs/aar') + into('app/libs/release') + include('godot-lib.release.aar') +} +/** + * Generate Godot custom build template by zipping the source files from the app directory, as well + * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'. + * The zip file also includes some gradle tools to allow building of the custom build. + */ +task zipCustomBuild(type: Zip) { + dependsOn ':generateGodotTemplates' + doFirst { + logger.lifecycle("Generating Godot custom build template") + } + from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradle.properties','gradlew', 'gradlew.bat', 'gradle/**'])) + include '**/*' + archiveName 'android_source.zip' + destinationDir(file(binDir)) +} + +/** + * Master task used to coordinate the tasks defined above to generate the set of Godot templates. + */ +task generateGodotTemplates(type: GradleBuild) { + // We exclude these gradle tasks so we can run the scons command manually. + for (String buildType : supportedTargets.keySet()) { + startParameter.excludedTaskNames += ":lib:" + getSconsTaskName(buildType) + } + + tasks = [] + + // Only build the apks and aar files for which we have native shared libraries. + for (String target : supportedTargets.keySet()) { + File targetLibs = new File("lib/libs/" + target) + if (targetLibs != null && targetLibs.isDirectory()) { + File[] targetLibsContents = targetLibs.listFiles() + if (targetLibsContents != null && targetLibsContents.length > 0) { + // Copy the generated aar library files to the custom build directory. + tasks += "copy" + target.capitalize() + "AAR" + // Copy the prebuilt binary templates to the bin directory. + tasks += "copy" + target.capitalize() + "BinaryToBin" + } + } + } + + finalizedBy 'zipCustomBuild' +} + +/** + * Clean the generated artifacts. + */ +task cleanGodotTemplates(type: Delete) { + // Delete the generated native libs + delete("lib/libs") + + // Delete the library generated AAR files + delete("lib/build/outputs/aar") + // Delete the app libs directory contents + delete("app/libs") + + // Delete the generated binary apks + delete("app/build/outputs/apk") + + // Delete the Godot templates in the Godot bin directory + delete("$binDir/android_debug.apk") + delete("$binDir/android_release.apk") + delete("$binDir/android_source.zip") +} diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index bf3de21830..bf50265715 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Sep 02 02:44:30 PDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml new file mode 100644 index 0000000000..517fc403b2 --- /dev/null +++ b/platform/android/java/lib/AndroidManifest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.godotengine.godot" + android:versionCode="1" + android:versionName="1.0"> + + <application> + + <service android:name=".GodotDownloaderService" /> + + </application> + + <instrumentation + android:icon="@drawable/icon" + android:label="@string/godot_project_name_string" + android:name=".GodotInstrumentation" + android:targetPackage="org.godotengine.godot" /> + +</manifest> diff --git a/platform/android/java/lib/CMakeLists.txt b/platform/android/java/lib/CMakeLists.txt new file mode 100644 index 0000000000..d3bdf6a5f2 --- /dev/null +++ b/platform/android/java/lib/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.6) +project(godot) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(GODOT_ROOT_DIR ../../../..) + +# Get sources +file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**) +file(GLOB_RECURSE HEADERS ${GODOT_ROOT_DIR}/*.h**) + +add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) +target_include_directories(${PROJECT_NAME} + SYSTEM PUBLIC + ${GODOT_ROOT_DIR} + ${GODOT_ROOT_DIR}/modules/gdnative/include) diff --git a/platform/android/java/lib/THIRDPARTY.md b/platform/android/java/lib/THIRDPARTY.md new file mode 100644 index 0000000000..2496b59263 --- /dev/null +++ b/platform/android/java/lib/THIRDPARTY.md @@ -0,0 +1,39 @@ +# Third-party libraries + +This file list third-party libraries used in the Android source folder, +with their provenance and, when relevant, modifications made to those files. + +## com.android.vending.billing + +- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main +- Version: git (7a94c69, 2019) +- License: Apache 2.0 + +Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`. + +## com.google.android.vending.expansion.downloader + +- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library +- Version: git (9ecf54e, 2017) +- License: Apache 2.0 + +Overwrite all files under: + +- `src/com/google/android/vending/expansion/downloader` + +Some files have been modified for yet unclear reasons. +See the `patches/com.google.android.vending.expansion.downloader.patch` file. + +## com.google.android.vending.licensing + +- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/ +- Version: git (eb57657, 2018) with modifications +- License: Apache 2.0 + +Overwrite all files under: + +- `aidl/com/android/vending/licensing` +- `src/com/google/android/vending/licensing` + +Some files have been modified to silence linter errors or fix downstream issues. +See the `patches/com.google.android.vending.licensing.patch` file. diff --git a/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl b/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000000..0f2bcae338 --- /dev/null +++ b/platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,281 @@ +/* + * 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.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog + * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down + * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion billing API version that the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the app is using + * @param packageName the package name of the calling app + * @param type of the in-app items ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", + * "type" : "inapp", + * "price" : "$5.00", + * "price_currency": "USD", + * "price_amount_micros": 5000000, + * "title : "Example Title", + * "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response + * codes on failures. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + on failures. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); + + /** + * This API is currently under development. + */ + int stub(int apiVersion, String packageName, String type); + + /** + * Returns a pending intent to launch the purchase flow for upgrading or downgrading a + * subscription. The existing owned SKU(s) should be provided along with the new SKU that + * the user is upgrading or downgrading to. + * @param apiVersion billing API version that the app is using, must be 5 or later + * @param packageName package name of the calling app + * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, + * if null or empty this method will behave like {@link #getBuyIntent} + * @param newSku the SKU that the user is upgrading or downgrading to + * @param type of the item being purchased, currently must be "subs" + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response + * codes on failures. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + */ + Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName, + in List<String> oldSkus, String newSku, String type, String developerPayload); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item. This method is + * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} + * parameter. This parameter is a Bundle of optional keys and values that affect the + * operation of the method. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @extraParams a Bundle with the following optional keys: + * "skusToReplace" - List<String> - an optional list of SKUs that the user is + * upgrading or downgrading from. + * Pass this field if the purchase is upgrading or downgrading + * existing subscriptions. + * The specified SKUs are replaced with the SKUs that the user is + * purchasing. Google Play replaces the specified SKUs at the start of + * the next billing cycle. + * "replaceSkusProration" - Boolean - whether the user should be credited for any unused + * subscription time on the SKUs they are upgrading or downgrading. + * If you set this field to true, Google Play swaps out the old SKUs + * and credits the user with the unused value of their subscription + * time on a pro-rated basis. + * Google Play applies this credit to the new subscription, and does + * not begin billing the user for the new subscription until after + * the credit is used up. + * If you set this field to false, the user does not receive credit for + * any unused subscription time and the recurrence date does not + * change. + * Default value is true. Ignored if you do not pass skusToReplace. + * "accountId" - String - an optional obfuscated string that is uniquely + * associated with the user's account in your app. + * If you pass this value, Google Play can use it to detect irregular + * activity, such as many devices making purchases on the same + * account in a short period of time. + * Do not use the developer ID or the user's Google ID for this field. + * In addition, this field should not contain the user's ID in + * cleartext. + * We recommend that you use a one-way hash to generate a string from + * the user's ID, and store the hashed string in this field. + * "vr" - Boolean - an optional flag indicating whether the returned intent + * should start a VR purchase flow. The apiVersion must also be 7 or + * later to use this flag. + */ + Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku, + String type, String developerPayload, in Bundle extraParams); + + /** + * Returns the most recent purchase made by the user for each SKU, even if that purchase is + * expired, canceled, or consumed. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus is too large, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @param extraParams a Bundle with extra params that would be appended into http request + * query string. Not used at this moment. Reserved for future functionality. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, + * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. + * + * "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchaseHistory(int apiVersion, String packageName, String type, + String continuationToken, in Bundle extraParams); + + /** + * This method is a variant of {@link #isBillingSupported}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 7 or later + * @param packageName package name of the calling app + * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" + * for subscriptions) + * @param extraParams a Bundle with the following optional keys: + * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams} + * supports returning a VR purchase flow. + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, + in Bundle extraParams); +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl index c816558afc..869cb16f68 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.aidl +++ b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl @@ -16,8 +16,6 @@ package com.android.vending.licensing; -// Android library projects do not yet support AIDL, so this has been -// precompiled into the src directory. oneway interface ILicenseResultListener { void verifyLicense(int responseCode, String signedData, String signature); } diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl index 664510ce0c..9541a2090c 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.aidl +++ b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl @@ -18,8 +18,6 @@ package com.android.vending.licensing; import com.android.vending.licensing.ILicenseResultListener; -// Android library projects do not yet support AIDL, so this has been -// precompiled into the src directory. oneway interface ILicensingService { void checkLicense(long nonce, String packageName, in ILicenseResultListener listener); } diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle new file mode 100644 index 0000000000..13a14422ed --- /dev/null +++ b/platform/android/java/lib/build.gradle @@ -0,0 +1,82 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation "com.android.support:support-core-utils:28.0.0" +} + +def pathToRootDir = "../../../../" + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + useLibrary 'org.apache.http.legacy' + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } + + lintOptions { + abortOnError false + disable 'MissingTranslation', 'UnusedResources' + } + + packagingOptions { + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + aidl.srcDirs = ['aidl'] + assets.srcDirs = ['assets'] + } + debug.jniLibs.srcDirs = ['libs/debug'] + release.jniLibs.srcDirs = ['libs/release'] + } + + libraryVariants.all { variant -> + variant.outputs.all { output -> + output.outputFileName = "godot-lib.${variant.name}.aar" + } + + def buildType = variant.buildType.name.capitalize() + + def taskPrefix = "" + if (project.path != ":") { + taskPrefix = project.path + ":" + } + + // Disable the externalNativeBuild* task as it would cause build failures since the cmake build + // files is only setup for editing support. + gradle.startParameter.excludedTaskNames += taskPrefix + "externalNativeBuild" + buildType + + def releaseTarget = supportedTargets[buildType.toLowerCase()] + if (releaseTarget == null || releaseTarget == "") { + throw new GradleException("Invalid build type: " + buildType) + } + + if (!supportedAbis.contains(defaultAbi)) { + throw new GradleException("Invalid default abi: " + defaultAbi) + } + + // Creating gradle task to generate the native libraries for the default abi. + def taskName = getSconsTaskName(buildType) + tasks.create(name: taskName, type: Exec) { + executable "scons" + sconsExt + args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() + } + + // Schedule the tasks so the generated libs are present before the aar file is packaged. + tasks["merge${buildType}JniLibFolders"].dependsOn taskName + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } +} diff --git a/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch b/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch new file mode 100644 index 0000000000..49cc41e817 --- /dev/null +++ b/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch @@ -0,0 +1,300 @@ +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +index ad6ea0de6..452c7d148 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +@@ -32,6 +32,9 @@ import android.os.Messenger; + import android.os.RemoteException; + import android.util.Log; + ++// -- GODOT start -- ++import java.lang.ref.WeakReference; ++// -- GODOT end -- + + + /** +@@ -118,29 +121,46 @@ public class DownloaderClientMarshaller { + /** + * Target we publish for clients to send messages to IncomingHandler. + */ +- final Messenger mMessenger = new Messenger(new Handler() { ++ // -- GODOT start -- ++ private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); ++ final Messenger mMessenger = new Messenger(mMsgHandler); ++ ++ private static class MessengerHandlerClient extends Handler { ++ private final WeakReference<Stub> mDownloader; ++ public MessengerHandlerClient(Stub downloader) { ++ mDownloader = new WeakReference<>(downloader); ++ } ++ + @Override + public void handleMessage(Message msg) { +- 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; ++ 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; ++ } ++ } ++ // -- GODOT end -- + + public Stub(IDownloaderClient itf, Class<?> downloaderService) { + mItf = itf; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +index 979352299..3771d19c9 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +@@ -25,6 +25,9 @@ import android.os.Message; + import android.os.Messenger; + import android.os.RemoteException; + ++// -- GODOT start -- ++import java.lang.ref.WeakReference; ++// -- GODOT end -- + + + /** +@@ -108,32 +111,49 @@ public class DownloaderServiceMarshaller { + + private static class Stub implements IStub { + private IDownloaderService mItf = null; +- final Messenger mMessenger = new Messenger(new Handler() { ++ // -- GODOT start -- ++ private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); ++ final Messenger mMessenger = new Messenger(mMsgHandler); ++ ++ private static class MessengerHandlerServer extends Handler { ++ private final WeakReference<Stub> mDownloader; ++ public MessengerHandlerServer(Stub downloader) { ++ mDownloader = new WeakReference<>(downloader); ++ } ++ + @Override + public void handleMessage(Message msg) { +- 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; ++ Stub downloader = mDownloader.get(); ++ if (downloader != null) { ++ downloader.handleMessage(msg); + } + } +- }); ++ } ++ ++ private void handleMessage(Message msg) { ++ switch (msg.what) { ++ case MSG_REQUEST_ABORT_DOWNLOAD: ++ mItf.requestAbortDownload(); ++ break; ++ case MSG_REQUEST_CONTINUE_DOWNLOAD: ++ mItf.requestContinueDownload(); ++ break; ++ case MSG_REQUEST_PAUSE_DOWNLOAD: ++ mItf.requestPauseDownload(); ++ break; ++ case MSG_SET_DOWNLOAD_FLAGS: ++ mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); ++ break; ++ case MSG_REQUEST_DOWNLOAD_STATE: ++ mItf.requestDownloadStatus(); ++ break; ++ case MSG_REQUEST_CLIENT_UPDATE: ++ mItf.onClientUpdated((Messenger)msg.getData().getParcelable( ++ PARAM_MESSENGER)); ++ break; ++ } ++ } ++ // -- GODOT end -- + + public Stub(IDownloaderService itf) { + mItf = itf; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +index e4b1b0f1c..36cd6aacf 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +@@ -24,7 +24,10 @@ import android.os.StatFs; + import android.os.SystemClock; + import android.util.Log; + +-import com.android.vending.expansion.downloader.R; ++// -- GODOT start -- ++//import com.android.vending.expansion.downloader.R; ++import org.godotengine.godot.R; ++// -- GODOT end -- + + import java.io.File; + import java.text.SimpleDateFormat; +@@ -146,12 +149,14 @@ public class Helpers { + } + return ""; + } +- return String.format("%.2f", ++ // -- GODOT start -- ++ return String.format(Locale.ENGLISH, "%.2f", + (float) overallProgress / (1024.0f * 1024.0f)) + + "MB /" + +- String.format("%.2f", (float) overallTotal / ++ String.format(Locale.ENGLISH, "%.2f", (float) overallTotal / + (1024.0f * 1024.0f)) + + "MB"; ++ // -- GODOT end -- + } + + /** +@@ -184,7 +189,9 @@ public class Helpers { + } + + public static String getSpeedString(float bytesPerMillisecond) { +- return String.format("%.2f", bytesPerMillisecond * 1000 / 1024); ++ // -- GODOT start -- ++ return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); ++ // -- GODOT end -- + } + + public static String getTimeRemaining(long durationInMilliseconds) { +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +index 12edd97ab..a0e1165cc 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +@@ -26,6 +26,10 @@ import android.net.NetworkInfo; + import android.telephony.TelephonyManager; + import android.util.Log; + ++// -- GODOT start -- ++import android.annotation.SuppressLint; ++// -- GODOT end -- ++ + /** + * Contains useful helper functions, typically tied to the application context. + */ +@@ -51,6 +55,7 @@ class SystemFacade { + return null; + } + ++ @SuppressLint("MissingPermission") + NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); + if (activeInfo == null) { + if (Constants.LOGVV) { +@@ -69,6 +74,7 @@ class SystemFacade { + return false; + } + ++ @SuppressLint("MissingPermission") + NetworkInfo info = connectivity.getActiveNetworkInfo(); + boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); + TelephonyManager tm = (TelephonyManager) mContext +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +index f1536e80e..4b214b22d 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +@@ -16,7 +16,11 @@ + + package com.google.android.vending.expansion.downloader.impl; + +-import com.android.vending.expansion.downloader.R; ++// -- GODOT start -- ++//import com.android.vending.expansion.downloader.R; ++import org.godotengine.godot.R; ++// -- GODOT end -- ++ + import com.google.android.vending.expansion.downloader.DownloadProgressInfo; + import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; + import com.google.android.vending.expansion.downloader.Helpers; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +index b2e0e7af0..c114b8a64 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +@@ -146,8 +146,12 @@ public class DownloadThread { + + try { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); +- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); +- wakeLock.acquire(); ++ // -- GODOT start -- ++ //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); ++ //wakeLock.acquire(); ++ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); ++ wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); ++ // -- GODOT end -- + + if (Constants.LOGV) { + Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +index 4babe476f..8d41a7690 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +@@ -50,6 +50,10 @@ import android.provider.Settings.Secure; + import android.telephony.TelephonyManager; + import android.util.Log; + ++// -- GODOT start -- ++import android.annotation.SuppressLint; ++// -- GODOT end -- ++ + import java.io.File; + + /** +@@ -578,6 +582,7 @@ public abstract class DownloaderService extends CustomIntentService implements I + Log.w(Constants.TAG, + "couldn't get connectivity manager to poll network state"); + } else { ++ @SuppressLint("MissingPermission") + NetworkInfo activeInfo = mConnectivityManager + .getActiveNetworkInfo(); + updateNetworkState(activeInfo); diff --git a/platform/android/java/lib/patches/com.google.android.vending.licensing.patch b/platform/android/java/lib/patches/com.google.android.vending.licensing.patch new file mode 100644 index 0000000000..4adb81d951 --- /dev/null +++ b/platform/android/java/lib/patches/com.google.android.vending.licensing.patch @@ -0,0 +1,42 @@ +diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java +index 7c42bfc28..feb579af0 100644 +--- a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java ++++ b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java +@@ -45,6 +45,9 @@ public class PreferenceObfuscator { + public void putString(String key, String value) { + if (mEditor == null) { + mEditor = mPreferences.edit(); ++ // -- GODOT start -- ++ mEditor.apply(); ++ // -- GODOT end -- + } + String obfuscatedValue = mObfuscator.obfuscate(value, key); + mEditor.putString(key, obfuscatedValue); +diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java +index a0d2779af..a8bf65f9c 100644 +--- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java ++++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java +@@ -31,6 +31,10 @@ package com.google.android.vending.licensing.util; + * @version 1.3 + */ + ++// -- GODOT start -- +import org.godotengine.godot.BuildConfig; ++// -- GODOT end -- ++ + /** + * Base64 converter class. This code is not a full-blown MIME encoder; + * it simply converts binary data to base64 data and back. +@@ -341,7 +345,11 @@ public class Base64 { + e += 4; + } + +- assert (e == outBuff.length); ++ // -- GODOT start -- ++ //assert (e == outBuff.length); ++ if (BuildConfig.DEBUG && e != outBuff.length) ++ throw new RuntimeException(); ++ // -- GODOT end -- + return outBuff; + } + diff --git a/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..f849d8e90d --- /dev/null +++ b/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..1dfb28b33a --- /dev/null +++ b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/drawable-nodpi/icon.png b/platform/android/java/lib/res/drawable-nodpi/icon.png Binary files differindex 6ad9b43117..6ad9b43117 100644 --- a/platform/android/java/res/drawable-nodpi/icon.png +++ b/platform/android/java/lib/res/drawable-nodpi/icon.png diff --git a/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png Binary files differindex 372b763ec5..372b763ec5 100644 --- a/platform/android/java/res/drawable-xhdpi/notify_panel_notification_icon_bg.png +++ b/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..302a972049 --- /dev/null +++ b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/res/layout/downloading_expansion.xml b/platform/android/java/lib/res/layout/downloading_expansion.xml index 4a9700965f..4a9700965f 100644 --- a/platform/android/java/res/layout/downloading_expansion.xml +++ b/platform/android/java/lib/res/layout/downloading_expansion.xml diff --git a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml index fae1faeb60..fae1faeb60 100644 --- a/platform/android/java/res/layout/status_bar_ongoing_event_progress_bar.xml +++ b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml diff --git a/platform/android/java/res/values-ar/strings.xml b/platform/android/java/lib/res/values-ar/strings.xml index 9f3dc6d6ac..9f3dc6d6ac 100644 --- a/platform/android/java/res/values-ar/strings.xml +++ b/platform/android/java/lib/res/values-ar/strings.xml diff --git a/platform/android/java/res/values-bg/strings.xml b/platform/android/java/lib/res/values-bg/strings.xml index bd8109277e..bd8109277e 100644 --- a/platform/android/java/res/values-bg/strings.xml +++ b/platform/android/java/lib/res/values-bg/strings.xml diff --git a/platform/android/java/res/values-ca/strings.xml b/platform/android/java/lib/res/values-ca/strings.xml index 494cb88468..494cb88468 100644 --- a/platform/android/java/res/values-ca/strings.xml +++ b/platform/android/java/lib/res/values-ca/strings.xml diff --git a/platform/android/java/res/values-cs/strings.xml b/platform/android/java/lib/res/values-cs/strings.xml index 30ce00f895..30ce00f895 100644 --- a/platform/android/java/res/values-cs/strings.xml +++ b/platform/android/java/lib/res/values-cs/strings.xml diff --git a/platform/android/java/res/values-da/strings.xml b/platform/android/java/lib/res/values-da/strings.xml index 4c2a1cf0f4..4c2a1cf0f4 100644 --- a/platform/android/java/res/values-da/strings.xml +++ b/platform/android/java/lib/res/values-da/strings.xml diff --git a/platform/android/java/res/values-de/strings.xml b/platform/android/java/lib/res/values-de/strings.xml index 52946d4cce..52946d4cce 100644 --- a/platform/android/java/res/values-de/strings.xml +++ b/platform/android/java/lib/res/values-de/strings.xml diff --git a/platform/android/java/res/values-el/strings.xml b/platform/android/java/lib/res/values-el/strings.xml index 181dc51762..181dc51762 100644 --- a/platform/android/java/res/values-el/strings.xml +++ b/platform/android/java/lib/res/values-el/strings.xml diff --git a/platform/android/java/res/values-en/strings.xml b/platform/android/java/lib/res/values-en/strings.xml index 976a565013..976a565013 100644 --- a/platform/android/java/res/values-en/strings.xml +++ b/platform/android/java/lib/res/values-en/strings.xml diff --git a/platform/android/java/res/values-es-rES/strings.xml b/platform/android/java/lib/res/values-es-rES/strings.xml index 73f63a08f8..73f63a08f8 100644 --- a/platform/android/java/res/values-es-rES/strings.xml +++ b/platform/android/java/lib/res/values-es-rES/strings.xml diff --git a/platform/android/java/res/values-es/strings.xml b/platform/android/java/lib/res/values-es/strings.xml index 07b718a641..07b718a641 100644 --- a/platform/android/java/res/values-es/strings.xml +++ b/platform/android/java/lib/res/values-es/strings.xml diff --git a/platform/android/java/res/values-fa/strings.xml b/platform/android/java/lib/res/values-fa/strings.xml index f1e29013c4..f1e29013c4 100644 --- a/platform/android/java/res/values-fa/strings.xml +++ b/platform/android/java/lib/res/values-fa/strings.xml diff --git a/platform/android/java/res/values-fi/strings.xml b/platform/android/java/lib/res/values-fi/strings.xml index 323d82aff1..323d82aff1 100644 --- a/platform/android/java/res/values-fi/strings.xml +++ b/platform/android/java/lib/res/values-fi/strings.xml diff --git a/platform/android/java/res/values-fr/strings.xml b/platform/android/java/lib/res/values-fr/strings.xml index 32bead2661..32bead2661 100644 --- a/platform/android/java/res/values-fr/strings.xml +++ b/platform/android/java/lib/res/values-fr/strings.xml diff --git a/platform/android/java/res/values-hi/strings.xml b/platform/android/java/lib/res/values-hi/strings.xml index 8aab2a8c63..8aab2a8c63 100644 --- a/platform/android/java/res/values-hi/strings.xml +++ b/platform/android/java/lib/res/values-hi/strings.xml diff --git a/platform/android/java/res/values-hr/strings.xml b/platform/android/java/lib/res/values-hr/strings.xml index caf55e2241..caf55e2241 100644 --- a/platform/android/java/res/values-hr/strings.xml +++ b/platform/android/java/lib/res/values-hr/strings.xml diff --git a/platform/android/java/res/values-hu/strings.xml b/platform/android/java/lib/res/values-hu/strings.xml index e7f9e51226..e7f9e51226 100644 --- a/platform/android/java/res/values-hu/strings.xml +++ b/platform/android/java/lib/res/values-hu/strings.xml diff --git a/platform/android/java/res/values-in/strings.xml b/platform/android/java/lib/res/values-in/strings.xml index 9e9a8b0c03..9e9a8b0c03 100644 --- a/platform/android/java/res/values-in/strings.xml +++ b/platform/android/java/lib/res/values-in/strings.xml diff --git a/platform/android/java/res/values-it/strings.xml b/platform/android/java/lib/res/values-it/strings.xml index 1f5e5a049e..1f5e5a049e 100644 --- a/platform/android/java/res/values-it/strings.xml +++ b/platform/android/java/lib/res/values-it/strings.xml diff --git a/platform/android/java/res/values-iw/strings.xml b/platform/android/java/lib/res/values-iw/strings.xml index f52ede2085..f52ede2085 100644 --- a/platform/android/java/res/values-iw/strings.xml +++ b/platform/android/java/lib/res/values-iw/strings.xml diff --git a/platform/android/java/res/values-ja/strings.xml b/platform/android/java/lib/res/values-ja/strings.xml index 7f85f57df7..7f85f57df7 100644 --- a/platform/android/java/res/values-ja/strings.xml +++ b/platform/android/java/lib/res/values-ja/strings.xml diff --git a/platform/android/java/res/values-ko/strings.xml b/platform/android/java/lib/res/values-ko/strings.xml index fab0bdd753..fab0bdd753 100644 --- a/platform/android/java/res/values-ko/strings.xml +++ b/platform/android/java/lib/res/values-ko/strings.xml diff --git a/platform/android/java/res/values-lt/strings.xml b/platform/android/java/lib/res/values-lt/strings.xml index 6e3677fde7..6e3677fde7 100644 --- a/platform/android/java/res/values-lt/strings.xml +++ b/platform/android/java/lib/res/values-lt/strings.xml diff --git a/platform/android/java/res/values-lv/strings.xml b/platform/android/java/lib/res/values-lv/strings.xml index 701fc271ac..701fc271ac 100644 --- a/platform/android/java/res/values-lv/strings.xml +++ b/platform/android/java/lib/res/values-lv/strings.xml diff --git a/platform/android/java/res/values-nb/strings.xml b/platform/android/java/lib/res/values-nb/strings.xml index 73147ca1af..73147ca1af 100644 --- a/platform/android/java/res/values-nb/strings.xml +++ b/platform/android/java/lib/res/values-nb/strings.xml diff --git a/platform/android/java/res/values-nl/strings.xml b/platform/android/java/lib/res/values-nl/strings.xml index e501928a35..e501928a35 100644 --- a/platform/android/java/res/values-nl/strings.xml +++ b/platform/android/java/lib/res/values-nl/strings.xml diff --git a/platform/android/java/res/values-pl/strings.xml b/platform/android/java/lib/res/values-pl/strings.xml index ea5da73b6f..ea5da73b6f 100644 --- a/platform/android/java/res/values-pl/strings.xml +++ b/platform/android/java/lib/res/values-pl/strings.xml diff --git a/platform/android/java/res/values-pt/strings.xml b/platform/android/java/lib/res/values-pt/strings.xml index bdda7cd2c7..bdda7cd2c7 100644 --- a/platform/android/java/res/values-pt/strings.xml +++ b/platform/android/java/lib/res/values-pt/strings.xml diff --git a/platform/android/java/res/values-ro/strings.xml b/platform/android/java/lib/res/values-ro/strings.xml index 3686da4c19..3686da4c19 100644 --- a/platform/android/java/res/values-ro/strings.xml +++ b/platform/android/java/lib/res/values-ro/strings.xml diff --git a/platform/android/java/res/values-ru/strings.xml b/platform/android/java/lib/res/values-ru/strings.xml index 954067658b..954067658b 100644 --- a/platform/android/java/res/values-ru/strings.xml +++ b/platform/android/java/lib/res/values-ru/strings.xml diff --git a/platform/android/java/res/values-sk/strings.xml b/platform/android/java/lib/res/values-sk/strings.xml index 37d1283124..37d1283124 100644 --- a/platform/android/java/res/values-sk/strings.xml +++ b/platform/android/java/lib/res/values-sk/strings.xml diff --git a/platform/android/java/res/values-sl/strings.xml b/platform/android/java/lib/res/values-sl/strings.xml index 0bb249c375..0bb249c375 100644 --- a/platform/android/java/res/values-sl/strings.xml +++ b/platform/android/java/lib/res/values-sl/strings.xml diff --git a/platform/android/java/res/values-sr/strings.xml b/platform/android/java/lib/res/values-sr/strings.xml index 0e83cab1a1..0e83cab1a1 100644 --- a/platform/android/java/res/values-sr/strings.xml +++ b/platform/android/java/lib/res/values-sr/strings.xml diff --git a/platform/android/java/res/values-sv/strings.xml b/platform/android/java/lib/res/values-sv/strings.xml index e3a04ac2ec..e3a04ac2ec 100644 --- a/platform/android/java/res/values-sv/strings.xml +++ b/platform/android/java/lib/res/values-sv/strings.xml diff --git a/platform/android/java/res/values-th/strings.xml b/platform/android/java/lib/res/values-th/strings.xml index 0aa893b8bf..0aa893b8bf 100644 --- a/platform/android/java/res/values-th/strings.xml +++ b/platform/android/java/lib/res/values-th/strings.xml diff --git a/platform/android/java/res/values-tl/strings.xml b/platform/android/java/lib/res/values-tl/strings.xml index e7e2af4909..e7e2af4909 100644 --- a/platform/android/java/res/values-tl/strings.xml +++ b/platform/android/java/lib/res/values-tl/strings.xml diff --git a/platform/android/java/res/values-tr/strings.xml b/platform/android/java/lib/res/values-tr/strings.xml index 97af1243a6..97af1243a6 100644 --- a/platform/android/java/res/values-tr/strings.xml +++ b/platform/android/java/lib/res/values-tr/strings.xml diff --git a/platform/android/java/res/values-uk/strings.xml b/platform/android/java/lib/res/values-uk/strings.xml index 3dea6908a9..3dea6908a9 100644 --- a/platform/android/java/res/values-uk/strings.xml +++ b/platform/android/java/lib/res/values-uk/strings.xml diff --git a/platform/android/java/res/values-vi/strings.xml b/platform/android/java/lib/res/values-vi/strings.xml index a6552130b0..a6552130b0 100644 --- a/platform/android/java/res/values-vi/strings.xml +++ b/platform/android/java/lib/res/values-vi/strings.xml diff --git a/platform/android/java/res/values-zh-rCN/strings.xml b/platform/android/java/lib/res/values-zh-rCN/strings.xml index 6668c56bd9..6668c56bd9 100644 --- a/platform/android/java/res/values-zh-rCN/strings.xml +++ b/platform/android/java/lib/res/values-zh-rCN/strings.xml diff --git a/platform/android/java/res/values-zh-rHK/strings.xml b/platform/android/java/lib/res/values-zh-rHK/strings.xml index 8a6269da0f..8a6269da0f 100644 --- a/platform/android/java/res/values-zh-rHK/strings.xml +++ b/platform/android/java/lib/res/values-zh-rHK/strings.xml diff --git a/platform/android/java/res/values-zh-rTW/strings.xml b/platform/android/java/lib/res/values-zh-rTW/strings.xml index b1bb39d5d6..b1bb39d5d6 100644 --- a/platform/android/java/res/values-zh-rTW/strings.xml +++ b/platform/android/java/lib/res/values-zh-rTW/strings.xml diff --git a/platform/android/java/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml index a1b81a6186..a1b81a6186 100644 --- a/platform/android/java/res/values/strings.xml +++ b/platform/android/java/lib/res/values/strings.xml diff --git a/platform/android/java/res/values/styles.xml b/platform/android/java/lib/res/values/styles.xml index a442f61e7e..a442f61e7e 100644 --- a/platform/android/java/res/values/styles.xml +++ b/platform/android/java/lib/res/values/styles.xml diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java index ff1eee528f..1dcc370d83 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Constants.java +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java @@ -18,113 +18,115 @@ package com.google.android.vending.expansion.downloader; import java.io.File; + /** * Contains the internal constants that are used in the download manager. * As a general rule, modifying these constants should be done with care. */ public class Constants { - /** Tag used for debugging/logging */ - public static final String TAG = "LVLDL"; + /** Tag used for debugging/logging */ + public static final String TAG = "LVLDL"; - /** + /** * Expansion path where we store obb files */ - public static final String EXP_PATH = File.separator + "Android" + File.separator + "obb" + File.separator; + public static final String EXP_PATH = File.separator + "Android" + + File.separator + "obb" + File.separator; - /** The intent that gets sent when the service must wake up for a retry */ - public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; + /** The intent that gets sent when the service must wake up for a retry */ + public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; - /** the intent that gets sent when clicking a successful download */ - public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; + /** the intent that gets sent when clicking a successful download */ + public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; - /** the intent that gets sent when clicking an incomplete/failed download */ - public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; + /** the intent that gets sent when clicking an incomplete/failed download */ + public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; - /** the intent that gets sent when deleting the notification of a completed download */ - public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; + /** the intent that gets sent when deleting the notification of a completed download */ + public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; - /** + /** * When a number has to be appended to the filename, this string is used to separate the * base filename from the sequence number */ - public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; + public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; - /** The default user agent used for downloads */ - public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; + /** The default user agent used for downloads */ + public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; - /** The buffer size used to stream the data */ - public static final int BUFFER_SIZE = 4096; + /** The buffer size used to stream the data */ + public static final int BUFFER_SIZE = 4096; - /** The minimum amount of progress that has to be done before the progress bar gets updated */ - public static final int MIN_PROGRESS_STEP = 4096; + /** The minimum amount of progress that has to be done before the progress bar gets updated */ + public static final int MIN_PROGRESS_STEP = 4096; - /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ - public static final long MIN_PROGRESS_TIME = 1000; + /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ + public static final long MIN_PROGRESS_TIME = 1000; - /** The maximum number of rows in the database (FIFO) */ - public static final int MAX_DOWNLOADS = 1000; + /** The maximum number of rows in the database (FIFO) */ + public static final int MAX_DOWNLOADS = 1000; - /** + /** * The number of times that the download manager will retry its network * operations when no progress is happening before it gives up. */ - public static final int MAX_RETRIES = 5; + public static final int MAX_RETRIES = 5; - /** + /** * The minimum amount of time that the download manager accepts for * a Retry-After response header with a parameter in delta-seconds. */ - public static final int MIN_RETRY_AFTER = 30; // 30s + public static final int MIN_RETRY_AFTER = 30; // 30s - /** + /** * The maximum amount of time that the download manager accepts for * a Retry-After response header with a parameter in delta-seconds. */ - public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h + public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h - /** + /** * The maximum number of redirects. */ - public static final int MAX_REDIRECTS = 5; // can't be more than 7. + public static final int MAX_REDIRECTS = 5; // can't be more than 7. - /** + /** * The time between a failure and the first retry after an IOException. * Each subsequent retry grows exponentially, doubling each time. * The time is in seconds. */ - public static final int RETRY_FIRST_DELAY = 30; + public static final int RETRY_FIRST_DELAY = 30; - /** Enable separate connectivity logging */ - public static final boolean LOGX = true; + /** Enable separate connectivity logging */ + public static final boolean LOGX = true; - /** Enable verbose logging */ - public static final boolean LOGV = false; + /** Enable verbose logging */ + public static final boolean LOGV = false; - /** Enable super-verbose logging */ - private static final boolean LOCAL_LOGVV = false; - public static final boolean LOGVV = LOCAL_LOGVV && LOGV; + /** Enable super-verbose logging */ + private static final boolean LOCAL_LOGVV = false; + public static final boolean LOGVV = LOCAL_LOGVV && LOGV; - /** + /** * This download has successfully completed. * Warning: there might be other status values that indicate success * in the future. * Use isSucccess() to capture the entire category. */ - public static final int STATUS_SUCCESS = 200; + public static final int STATUS_SUCCESS = 200; - /** + /** * This request couldn't be parsed. This is also used when processing * requests with unknown/unsupported URI schemes. */ - public static final int STATUS_BAD_REQUEST = 400; + public static final int STATUS_BAD_REQUEST = 400; - /** + /** * This download can't be performed because the content type cannot be * handled. */ - public static final int STATUS_NOT_ACCEPTABLE = 406; + public static final int STATUS_NOT_ACCEPTABLE = 406; - /** + /** * This download cannot be performed because the length cannot be * determined accurately. This is the code for the HTTP error "Length * Required", which is typically used when making requests that require @@ -133,101 +135,102 @@ public class Constants { * accurately (therefore making it impossible to know when a download * completes). */ - public static final int STATUS_LENGTH_REQUIRED = 411; + public static final int STATUS_LENGTH_REQUIRED = 411; - /** + /** * This download was interrupted and cannot be resumed. * This is the code for the HTTP error "Precondition Failed", and it is * also used in situations where the client doesn't have an ETag at all. */ - public static final int STATUS_PRECONDITION_FAILED = 412; + public static final int STATUS_PRECONDITION_FAILED = 412; - /** + /** * The lowest-valued error status that is not an actual HTTP status code. */ - public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; + public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; - /** + /** * The requested destination file already exists. */ - public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; - /** + /** * Some possibly transient error occurred, but we can't resume the download. */ - public static final int STATUS_CANNOT_RESUME = 489; + public static final int STATUS_CANNOT_RESUME = 489; - /** + /** * This download was canceled */ - public static final int STATUS_CANCELED = 490; + public static final int STATUS_CANCELED = 490; - /** + /** * This download has completed with an error. * Warning: there will be other status values that indicate errors in * the future. Use isStatusError() to capture the entire category. */ - public static final int STATUS_UNKNOWN_ERROR = 491; + public static final int STATUS_UNKNOWN_ERROR = 491; - /** + /** * This download couldn't be completed because of a storage issue. * Typically, that's because the filesystem is missing or full. * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. */ - public static final int STATUS_FILE_ERROR = 492; + public static final int STATUS_FILE_ERROR = 492; - /** + /** * This download couldn't be completed because of an HTTP * redirect response that the download manager couldn't * handle. */ - public static final int STATUS_UNHANDLED_REDIRECT = 493; + public static final int STATUS_UNHANDLED_REDIRECT = 493; - /** + /** * This download couldn't be completed because of an * unspecified unhandled HTTP code. */ - public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + public static final int STATUS_UNHANDLED_HTTP_CODE = 494; - /** + /** * This download couldn't be completed because of an * error receiving or processing data at the HTTP level. */ - public static final int STATUS_HTTP_DATA_ERROR = 495; + public static final int STATUS_HTTP_DATA_ERROR = 495; - /** + /** * This download couldn't be completed because of an * HttpException while setting up the request. */ - public static final int STATUS_HTTP_EXCEPTION = 496; + public static final int STATUS_HTTP_EXCEPTION = 496; - /** + /** * This download couldn't be completed because there were * too many redirects. */ - public static final int STATUS_TOO_MANY_REDIRECTS = 497; + public static final int STATUS_TOO_MANY_REDIRECTS = 497; - /** + /** * This download couldn't be completed due to insufficient storage * space. Typically, this is because the SD card is full. */ - public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; - /** + /** * This download couldn't be completed because no external storage * device was found. Typically, this is because the SD card is not * mounted. */ - public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; - /** + /** * The wake duration to check to see if a download is possible. */ - public static final long WATCHDOG_WAKE_TIMER = 60 * 1000; + public static final long WATCHDOG_WAKE_TIMER = 60*1000; - /** + /** * The wake duration to check to see if the process was killed. */ - public static final long ACTIVE_THREAD_WATCHDOG = 5 * 1000; + public static final long ACTIVE_THREAD_WATCHDOG = 5*1000; + }
\ No newline at end of file diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java new file mode 100644 index 0000000000..9cb294d721 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java @@ -0,0 +1,80 @@ +/* + * 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; + +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * This class contains progress information about the active download(s). + * + * When you build the Activity that initiates a download and tracks the + * progress by implementing the {@link IDownloaderClient} interface, you'll + * receive a DownloadProgressInfo object in each call to the {@link + * IDownloaderClient#onDownloadProgress} method. This allows you to update + * your activity's UI with information about the download progress, such + * as the progress so far, time remaining and current speed. + */ +public class DownloadProgressInfo implements Parcelable { + public long mOverallTotal; + public long mOverallProgress; + public long mTimeRemaining; // time remaining + public float mCurrentSpeed; // speed in KB/S + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int i) { + p.writeLong(mOverallTotal); + p.writeLong(mOverallProgress); + p.writeLong(mTimeRemaining); + p.writeFloat(mCurrentSpeed); + } + + public DownloadProgressInfo(Parcel p) { + mOverallTotal = p.readLong(); + mOverallProgress = p.readLong(); + mTimeRemaining = p.readLong(); + mCurrentSpeed = p.readFloat(); + } + + public DownloadProgressInfo(long overallTotal, long overallProgress, + long timeRemaining, + float currentSpeed) { + this.mOverallTotal = overallTotal; + this.mOverallProgress = overallProgress; + this.mTimeRemaining = timeRemaining; + this.mCurrentSpeed = currentSpeed; + } + + public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { + @Override + public DownloadProgressInfo createFromParcel(Parcel parcel) { + return new DownloadProgressInfo(parcel); + } + + @Override + public DownloadProgressInfo[] newArray(int i) { + return new DownloadProgressInfo[i]; + } + }; + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java new file mode 100644 index 0000000000..452c7d1483 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java @@ -0,0 +1,297 @@ +/* + * 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; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.app.PendingIntent; +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.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +// -- GODOT start -- +import java.lang.ref.WeakReference; +// -- GODOT end -- + + +/** + * This class binds the service API to your application client. It contains the IDownloaderClient proxy, + * which is used to call functions in your client as well as the Stub, which is used to call functions + * in the client implementation of IDownloaderClient. + * + * <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method + * should be called whenever the client wants to bind to the service. It opens up a service connection + * that ends up calling the onServiceConnected client API that passes the service messenger + * in. If the client wants to be notified by the service, it is responsible for then passing its + * messenger to the service in a separate call. + * + * <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}. + * + * <p>When your application first starts, you should first check whether your app's expansion files are + * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which + * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method + * returns a value indicating whether download is required or not. + * + * <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through + * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link + * IStub} object that you need in order to receive calls through your {@link IDownloaderClient} + * interface. + */ +public class DownloaderClientMarshaller { + public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; + public static final int MSG_ONDOWNLOADPROGRESS = 11; + public static final int MSG_ONSERVICECONNECTED = 12; + + public static final String PARAM_NEW_STATE = "newState"; + public static final String PARAM_PROGRESS = "progress"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; + public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; + public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; + + private static class Proxy implements IDownloaderClient { + private Messenger mServiceMessenger; + + @Override + public void onDownloadStateChanged(int newState) { + Bundle params = new Bundle(1); + params.putInt(PARAM_NEW_STATE, newState); + send(MSG_ONDOWNLOADSTATE_CHANGED, params); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + Bundle params = new Bundle(1); + params.putParcelable(PARAM_PROGRESS, progress); + send(MSG_ONDOWNLOADPROGRESS, params); + } + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mServiceMessenger.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mServiceMessenger = msg; + } + + @Override + public void onServiceConnected(Messenger m) { + /** + * This is never called through the proxy. + */ + } + } + + private static class Stub implements IStub { + private IDownloaderClient mItf = null; + private Class<?> mDownloaderServiceClass; + private boolean mBound; + private Messenger mServiceMessenger; + private Context mContext; + /** + * Target we publish for clients to send messages to IncomingHandler. + */ + // -- GODOT start -- + private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); + final Messenger mMessenger = new Messenger(mMsgHandler); + + private static class MessengerHandlerClient extends Handler { + private final WeakReference<Stub> mDownloader; + public MessengerHandlerClient(Stub downloader) { + mDownloader = new WeakReference<>(downloader); + } + + @Override + public void handleMessage(Message msg) { + Stub downloader = mDownloader.get(); + if (downloader != null) { + downloader.handleMessage(msg); + } + } + } + + 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; + } + } + // -- GODOT end -- + + public Stub(IDownloaderClient itf, Class<?> downloaderService) { + mItf = itf; + mDownloaderServiceClass = downloaderService; + } + + /** + * Class for interacting with the main interface of the service. + */ + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + mServiceMessenger = new Messenger(service); + mItf.onServiceConnected( + mServiceMessenger); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + mServiceMessenger = null; + } + }; + + @Override + public void connect(Context c) { + mContext = c; + Intent bindIntent = new Intent(c, mDownloaderServiceClass); + bindIntent.putExtra(PARAM_MESSENGER, mMessenger); + if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) { + if ( Constants.LOGVV ) { + Log.d(Constants.TAG, "Service Unbound"); + } + } else { + mBound = true; + } + + } + + @Override + public void disconnect(Context c) { + if (mBound) { + c.unbindService(mConnection); + mBound = false; + } + mContext = null; + } + + @Override + public Messenger getMessenger() { + return mMessenger; + } + } + + /** + * Returns a proxy that will marshal calls to IDownloaderClient methods + * + * @param msg + * @return + */ + public static IDownloaderClient CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshaled + * {@link IDownloaderClient} methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderClient that will be called + * when remote method calls are unmarshaled. + * @param downloaderService The class for your implementation of {@link + * impl.DownloaderService}. + * @return The {@link IStub} that allows you to connect to the service such that + * your {@link IDownloaderClient} receives status updates. + */ + public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { + return new Stub(itf, downloaderService); + } + + /** + * Starts the download if necessary. This function starts a flow that does ` + * many things. 1) Checks to see if the APK version has been checked and + * the metadata database updated 2) If the APK version does not match, + * checks the new LVL status to see if a new download is required 3) If the + * APK version does match, then checks to see if the download(s) have been + * completed 4) If the downloads have been completed, returns + * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the + * startup of an application to quickly ascertain if the application needs + * to wait to hear about any updated APK expansion files. Note that this does + * mean that the application MUST be run for the first time with a network + * connection, even if Market delivers all of the files. + * + * @param context Your application Context. + * @param notificationClient A PendingIntent to start the Activity in your application + * that shows the download progress and which will also start the application when download + * completes. + * @param serviceClass the class of your {@link imp.DownloaderService} implementation + * @return whether the service was started and the reason for starting the service. + * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link + * #DOWNLOAD_REQUIRED}. + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, + Class<?> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + + /** + * This version assumes that the intent contains the pending intent as a parameter. This + * is used for responding to alarms. + * <p>The pending intent must be in an extra with the key {@link + * impl.DownloaderService#EXTRA_PENDING_INTENT}. + * + * @param context + * @param notificationClient + * @param serviceClass the class of the service to start + * @return + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, + Class<?> serviceClass) + throws NameNotFoundException { + return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, + serviceClass); + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java new file mode 100644 index 0000000000..3771d19c9b --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java @@ -0,0 +1,201 @@ +/* + * 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; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + +// -- GODOT start -- +import java.lang.ref.WeakReference; +// -- GODOT end -- + + +/** + * This class is used by the client activity to proxy requests to the Downloader + * Service. + * + * Most importantly, you must call {@link #CreateProxy} during the {@link + * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate + * an {@link IDownloaderService} object that you can then use to issue commands to the {@link + * DownloaderService} (such as to pause and resume downloads). + */ +public class DownloaderServiceMarshaller { + + public static final int MSG_REQUEST_ABORT_DOWNLOAD = + 1; + public static final int MSG_REQUEST_PAUSE_DOWNLOAD = + 2; + public static final int MSG_SET_DOWNLOAD_FLAGS = + 3; + public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = + 4; + public static final int MSG_REQUEST_DOWNLOAD_STATE = + 5; + public static final int MSG_REQUEST_CLIENT_UPDATE = + 6; + + public static final String PARAMS_FLAGS = "flags"; + public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + + private static class Proxy implements IDownloaderService { + private Messenger mMsg; + + private void send(int method, Bundle params) { + Message m = Message.obtain(null, method); + m.setData(params); + try { + mMsg.send(m); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + public Proxy(Messenger msg) { + mMsg = msg; + } + + @Override + public void requestAbortDownload() { + send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); + } + + @Override + public void requestPauseDownload() { + send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); + } + + @Override + public void setDownloadFlags(int flags) { + Bundle params = new Bundle(); + params.putInt(PARAMS_FLAGS, flags); + send(MSG_SET_DOWNLOAD_FLAGS, params); + } + + @Override + public void requestContinueDownload() { + send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); + } + + @Override + public void requestDownloadStatus() { + send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); + } + + @Override + public void onClientUpdated(Messenger clientMessenger) { + Bundle bundle = new Bundle(1); + bundle.putParcelable(PARAM_MESSENGER, clientMessenger); + send(MSG_REQUEST_CLIENT_UPDATE, bundle); + } + } + + private static class Stub implements IStub { + private IDownloaderService mItf = null; + // -- GODOT start -- + private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); + final Messenger mMessenger = new Messenger(mMsgHandler); + + private static class MessengerHandlerServer extends Handler { + private final WeakReference<Stub> mDownloader; + public MessengerHandlerServer(Stub downloader) { + mDownloader = new WeakReference<>(downloader); + } + + @Override + public void handleMessage(Message msg) { + Stub downloader = mDownloader.get(); + if (downloader != null) { + downloader.handleMessage(msg); + } + } + } + + private void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REQUEST_ABORT_DOWNLOAD: + mItf.requestAbortDownload(); + break; + case MSG_REQUEST_CONTINUE_DOWNLOAD: + mItf.requestContinueDownload(); + break; + case MSG_REQUEST_PAUSE_DOWNLOAD: + mItf.requestPauseDownload(); + break; + case MSG_SET_DOWNLOAD_FLAGS: + mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); + break; + case MSG_REQUEST_DOWNLOAD_STATE: + mItf.requestDownloadStatus(); + break; + case MSG_REQUEST_CLIENT_UPDATE: + mItf.onClientUpdated((Messenger)msg.getData().getParcelable( + PARAM_MESSENGER)); + break; + } + } + // -- GODOT end -- + + public Stub(IDownloaderService itf) { + mItf = itf; + } + + @Override + public Messenger getMessenger() { + return mMessenger; + } + + @Override + public void connect(Context c) { + + } + + @Override + public void disconnect(Context c) { + + } + } + + /** + * Returns a proxy that will marshall calls to IDownloaderService methods + * + * @param ctx + * @return + */ + public static IDownloaderService CreateProxy(Messenger msg) { + return new Proxy(msg); + } + + /** + * Returns a stub object that, when connected, will listen for marshalled + * IDownloaderService methods and translate them into calls to the supplied + * interface. + * + * @param itf An implementation of IDownloaderService that will be called + * when remote method calls are unmarshalled. + * @return + */ + public static IStub CreateStub(IDownloaderService itf) { + return new Stub(itf); + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java new file mode 100644 index 0000000000..2a72c9818d --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java @@ -0,0 +1,367 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.os.SystemClock; +import android.util.Log; + +// -- GODOT start -- +//import com.android.vending.expansion.downloader.R; +import org.godotengine.godot.R; +// -- GODOT end -- + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Some helper functions for the download manager + */ +public class Helpers { + + public static Random sRandom = new Random(SystemClock.uptimeMillis()); + + /** Regex used to parse content-disposition headers */ + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern + .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + + private Helpers() { + } + + /* + * Parse the Content-Disposition HTTP Header. The format of the header is defined here: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for + * content that is going to be downloaded to the file system. We only support the attachment + * type. + */ + static String parseContentDisposition(String contentDisposition) { + try { + Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); + if (m.find()) { + return m.group(1); + } + } catch (IllegalStateException ex) { + // This function is defined as returning null when it can't parse + // the header + } + return null; + } + + /** + * @return the root of the filesystem containing the given path + */ + public static File getFilesystemRoot(String path) { + File cache = Environment.getDownloadCacheDirectory(); + if (path.startsWith(cache.getPath())) { + return cache; + } + File external = Environment.getExternalStorageDirectory(); + if (path.startsWith(external.getPath())) { + return external; + } + throw new IllegalArgumentException( + "Cannot determine filesystem root for " + path); + } + + public static boolean isExternalMediaMounted() { + if (!Environment.getExternalStorageState().equals( + Environment.MEDIA_MOUNTED)) { + // No SD card found. + if (Constants.LOGVV) { + Log.d(Constants.TAG, "no external storage"); + } + return false; + } + return true; + } + + /** + * @return the number of bytes available on the filesystem rooted at the given File + */ + public static long getAvailableBytes(File root) { + StatFs stat = new StatFs(root.getPath()); + // put a bit of margin (in case creating the file grows the system by a + // few blocks) + long availableBlocks = (long) stat.getAvailableBlocks() - 4; + return stat.getBlockSize() * availableBlocks; + } + + /** + * Checks whether the filename looks legitimate + */ + public static boolean isFilenameValid(String filename) { + filename = filename.replaceFirst("/+", "/"); // normalize leading + // slashes + return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) + || filename.startsWith(Environment.getExternalStorageDirectory().toString()); + } + + /* + * Delete the given file from device + */ + /* package */static void deleteFile(String path) { + try { + File file = new File(path); + file.delete(); + } catch (Exception e) { + Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); + } + } + + /** + * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total + * file size, but given what we know about the expected ranges of file sizes for APK expansion + * files, it's probably not necessary. + * + * @param overallProgress + * @param overallTotal + * @return + */ + + static public String getDownloadProgressString(long overallProgress, long overallTotal) { + if (overallTotal == 0) { + if (Constants.LOGVV) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + // -- GODOT start -- + return String.format(Locale.ENGLISH, "%.2f", + (float) overallProgress / (1024.0f * 1024.0f)) + + "MB /" + + String.format(Locale.ENGLISH, "%.2f", (float) overallTotal / + (1024.0f * 1024.0f)) + + "MB"; + // -- GODOT end -- + } + + /** + * Adds a percentile to getDownloadProgressString. + * + * @param overallProgress + * @param overallTotal + * @return + */ + static public String getDownloadProgressStringNotification(long overallProgress, + long overallTotal) { + if (overallTotal == 0) { + if (Constants.LOGVV) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return getDownloadProgressString(overallProgress, overallTotal) + " (" + + getDownloadProgressPercent(overallProgress, overallTotal) + ")"; + } + + public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { + if (overallTotal == 0) { + if (Constants.LOGVV) { + Log.e(Constants.TAG, "Notification called when total is zero"); + } + return ""; + } + return Long.toString(overallProgress * 100 / overallTotal) + "%"; + } + + public static String getSpeedString(float bytesPerMillisecond) { + // -- GODOT start -- + return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); + // -- GODOT end -- + } + + public static String getTimeRemaining(long durationInMilliseconds) { + SimpleDateFormat sdf; + if (durationInMilliseconds > 1000 * 60 * 60) { + sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); + } else { + sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); + } + return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); + } + + /** + * Returns the file name (without full path) for an Expansion APK file from the given context. + * + * @param c the context + * @param mainFile true for main file, false for patch file + * @param versionCode the version of the file + * @return String the file name of the expansion file + */ + public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { + return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; + } + + /** + * Returns the filename (where the file should be saved) from info about a download + */ + static public String generateSaveFileName(Context c, String fileName) { + String path = getSaveFilePath(c) + + File.separator + fileName; + return path; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + static public String getSaveFilePath(Context c) { + // This technically existed since Honeycomb, but it is critical + // on KitKat and greater versions since it will create the + // directory if needed + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return c.getObbDir().toString(); + } else { + File root = Environment.getExternalStorageDirectory(); + String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); + return path; + } + } + + /** + * Helper function to ascertain the existence of a file and return true/false appropriately + * + * @param c the app/activity/service context + * @param fileName the name (sans path) of the file to query + * @param fileSize the size that the file must match + * @param deleteFileOnMismatch if the file sizes do not match, delete the file + * @return true if it does exist, false otherwise + */ + static public boolean doesFileExist(Context c, String fileName, long fileSize, + boolean deleteFileOnMismatch) { + // the file may have been delivered by Play --- let's make sure + // it's the size we expect + File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); + if (fileForNewFile.exists()) { + if (fileForNewFile.length() == fileSize) { + return true; + } + if (deleteFileOnMismatch) { + // delete the file --- we won't be able to resume + // because we cannot confirm the integrity of the file + fileForNewFile.delete(); + } + } + return false; + } + + public static final int FS_READABLE = 0; + public static final int FS_DOES_NOT_EXIST = 1; + public static final int FS_CANNOT_READ = 2; + + /** + * Helper function to ascertain whether a file can be read. + * + * @param c the app/activity/service context + * @param fileName the name (sans path) of the file to query + * @return true if it does exist, false otherwise + */ + static public int getFileStatus(Context c, String fileName) { + // the file may have been delivered by Play --- let's make sure + // it's the size we expect + File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); + int returnValue; + if (fileForNewFile.exists()) { + if (fileForNewFile.canRead()) { + returnValue = FS_READABLE; + } else { + returnValue = FS_CANNOT_READ; + } + } else { + returnValue = FS_DOES_NOT_EXIST; + } + return returnValue; + } + + /** + * Helper function to ascertain whether the application has the correct access to the OBB + * directory to allow an OBB file to be written. + * + * @param c the app/activity/service context + * @return true if the application can write an OBB file, false otherwise + */ + static public boolean canWriteOBBFile(Context c) { + String path = getSaveFilePath(c); + File fileForNewFile = new File(path); + boolean canWrite; + if (fileForNewFile.exists()) { + canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite(); + } else { + canWrite = fileForNewFile.mkdirs(); + } + return canWrite; + } + + /** + * Converts download states that are returned by the + * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful + * if using the state strings built into the library to display user messages. + * + * @param state One of the STATE_* constants from {@link IDownloaderClient}. + * @return string resource ID for the corresponding string. + */ + static public int getDownloaderStringResourceIDFromState(int state) { + switch (state) { + case IDownloaderClient.STATE_IDLE: + return R.string.state_idle; + case IDownloaderClient.STATE_FETCHING_URL: + return R.string.state_fetching_url; + case IDownloaderClient.STATE_CONNECTING: + return R.string.state_connecting; + case IDownloaderClient.STATE_DOWNLOADING: + return R.string.state_downloading; + case IDownloaderClient.STATE_COMPLETED: + return R.string.state_completed; + case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: + return R.string.state_paused_network_unavailable; + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + return R.string.state_paused_by_request; + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: + return R.string.state_paused_wifi_disabled; + case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: + return R.string.state_paused_wifi_unavailable; + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: + return R.string.state_paused_wifi_disabled; + case IDownloaderClient.STATE_PAUSED_NEED_WIFI: + return R.string.state_paused_wifi_unavailable; + case IDownloaderClient.STATE_PAUSED_ROAMING: + return R.string.state_paused_roaming; + case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: + return R.string.state_paused_network_setup_failure; + case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: + return R.string.state_paused_sdcard_unavailable; + case IDownloaderClient.STATE_FAILED_UNLICENSED: + return R.string.state_failed_unlicensed; + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + return R.string.state_failed_fetching_url; + case IDownloaderClient.STATE_FAILED_SDCARD_FULL: + return R.string.state_failed_sdcard_full; + case IDownloaderClient.STATE_FAILED_CANCELED: + return R.string.state_failed_cancelled; + default: + return R.string.state_unknown; + } + } + +} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java index bae93f633a..cef3794701 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -23,26 +23,26 @@ import android.os.Messenger; * downloader. It is used to pass status from the service to the client. */ public interface IDownloaderClient { - static final int STATE_IDLE = 1; - static final int STATE_FETCHING_URL = 2; - static final int STATE_CONNECTING = 3; - static final int STATE_DOWNLOADING = 4; - static final int STATE_COMPLETED = 5; + static final int STATE_IDLE = 1; + static final int STATE_FETCHING_URL = 2; + static final int STATE_CONNECTING = 3; + static final int STATE_DOWNLOADING = 4; + static final int STATE_COMPLETED = 5; - static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; - static final int STATE_PAUSED_BY_REQUEST = 7; + static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; + static final int STATE_PAUSED_BY_REQUEST = 7; - /** + /** * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and * cellular permission will restart the service. Wi-Fi disabled means that * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the * other case Wi-Fi is enabled but not available. */ - static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; - static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; + static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; + static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; - /** + /** * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that * Wi-Fi is unavailable and cellular permission will NOT restart the * service. Wi-Fi disabled means that the Wi-Fi manager is returning that @@ -53,27 +53,27 @@ public interface IDownloaderClient { * developers with very large payloads do not allow these payloads to be * downloaded over cellular connections. */ - static final int STATE_PAUSED_WIFI_DISABLED = 10; - static final int STATE_PAUSED_NEED_WIFI = 11; + static final int STATE_PAUSED_WIFI_DISABLED = 10; + static final int STATE_PAUSED_NEED_WIFI = 11; - static final int STATE_PAUSED_ROAMING = 12; + static final int STATE_PAUSED_ROAMING = 12; - /** + /** * Scary case. We were on a network that redirected us to another website * that delivered us the wrong file. */ - static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; + static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; - static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; + static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; - static final int STATE_FAILED_UNLICENSED = 15; - static final int STATE_FAILED_FETCHING_URL = 16; - static final int STATE_FAILED_SDCARD_FULL = 17; - static final int STATE_FAILED_CANCELED = 18; + static final int STATE_FAILED_UNLICENSED = 15; + static final int STATE_FAILED_FETCHING_URL = 16; + static final int STATE_FAILED_SDCARD_FULL = 17; + static final int STATE_FAILED_CANCELED = 18; - static final int STATE_FAILED = 19; + static final int STATE_FAILED = 19; - /** + /** * Called internally by the stub when the service is bound to the client. * <p> * Critical implementation detail. In onServiceConnected we create the @@ -90,9 +90,9 @@ public interface IDownloaderClient { * @param m the service Messenger. This Messenger is used to call the * service API from the client. */ - void onServiceConnected(Messenger m); + void onServiceConnected(Messenger m); - /** + /** * Called when the download state changes. Depending on the state, there may * be user requests. The service is free to change the download state in the * middle of a user request, so the client should be able to handle this. @@ -112,9 +112,9 @@ public interface 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 * client UI. This progress should only be shown in a few states such as * STATE_DOWNLOADING. @@ -122,5 +122,5 @@ public interface IDownloaderClient { * @param progress the DownloadProgressInfo object containing the current * progress of all downloads. */ - void onDownloadProgress(DownloadProgressInfo progress); + void onDownloadProgress(DownloadProgressInfo progress); } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java index a84fb32728..4de9de0c62 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IDownloaderService.java +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -31,47 +31,47 @@ import android.os.Messenger; * should immediately call {@link #onClientUpdated}. */ public interface IDownloaderService { - /** + /** * Set this flag in response to the * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then * call RequestContinueDownload to resume a download */ - public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; + public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; - /** + /** * Request that the service abort the current download. The service should * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. */ - void requestAbortDownload(); + void requestAbortDownload(); - /** + /** * Request that the service pause the current download. The service should * respond by changing the state to * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. */ - void requestPauseDownload(); + void requestPauseDownload(); - /** + /** * Request that the service continue a paused download, when in any paused * or failed state, including * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. */ - void requestContinueDownload(); + void requestContinueDownload(); - /** + /** * Set the flags for this download (e.g. * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). * * @param flags */ - void setDownloadFlags(int flags); + void setDownloadFlags(int flags); - /** + /** * Requests that the download status be sent to the client. */ - void requestDownloadStatus(); + void requestDownloadStatus(); - /** + /** * Call this when you get {@link * IDownloaderClient.onServiceConnected(Messenger m)} from the * DownloaderClient to register the client with the service. It will @@ -79,5 +79,5 @@ public interface IDownloaderService { * * @param clientMessenger */ - void onClientUpdated(Messenger clientMessenger); + void onClientUpdated(Messenger clientMessenger); } diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java index dcdef1bfcf..d5bc3a843e 100644 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/IStub.java +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java @@ -33,9 +33,9 @@ import android.os.Messenger; * {@link IDownloaderService#onClientUpdated}. */ public interface IStub { - Messenger getMessenger(); + Messenger getMessenger(); - void connect(Context c); + void connect(Context c); - void disconnect(Context c); + void disconnect(Context c); } diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java new file mode 100644 index 0000000000..a0e1165cc4 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java @@ -0,0 +1,129 @@ +/* + * 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; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; +import android.util.Log; + +// -- GODOT start -- +import android.annotation.SuppressLint; +// -- GODOT end -- + +/** + * Contains useful helper functions, typically tied to the application context. + */ +class SystemFacade { + private Context mContext; + private NotificationManager mNotificationManager; + + public SystemFacade(Context context) { + mContext = context; + mNotificationManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + public Integer getActiveNetworkType() { + ConnectivityManager connectivity = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) { + Log.w(Constants.TAG, "couldn't get connectivity manager"); + return null; + } + + @SuppressLint("MissingPermission") + NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); + if (activeInfo == null) { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "network is not available"); + } + return null; + } + return activeInfo.getType(); + } + + public boolean isNetworkRoaming() { + ConnectivityManager connectivity = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivity == null) { + Log.w(Constants.TAG, "couldn't get connectivity manager"); + return false; + } + + @SuppressLint("MissingPermission") + NetworkInfo info = connectivity.getActiveNetworkInfo(); + boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); + TelephonyManager tm = (TelephonyManager) mContext + .getSystemService(Context.TELEPHONY_SERVICE); + if (null == tm) { + Log.w(Constants.TAG, "couldn't get telephony manager"); + return false; + } + boolean isRoaming = isMobile && tm.isNetworkRoaming(); + if (Constants.LOGVV && isRoaming) { + Log.v(Constants.TAG, "network is roaming"); + } + return isRoaming; + } + + public Long getMaxBytesOverMobile() { + return (long) Integer.MAX_VALUE; + } + + public Long getRecommendedMaxBytesOverMobile() { + return 2097152L; + } + + public void sendBroadcast(Intent intent) { + mContext.sendBroadcast(intent); + } + + public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { + return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; + } + + public void postNotification(long id, Notification notification) { + /** + * TODO: The system notification manager takes ints, not longs, as IDs, + * but the download manager uses IDs take straight from the database, + * which are longs. This will have to be dealt with at some point. + */ + mNotificationManager.notify((int) id, notification); + } + + public void cancelNotification(long id) { + mNotificationManager.cancel((int) id); + } + + public void cancelAllNotifications() { + mNotificationManager.cancelAll(); + } + + public void startThread(Thread thread) { + thread.start(); + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java new file mode 100644 index 0000000000..3ccc191c60 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java @@ -0,0 +1,112 @@ +/* + * 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 android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +/** + * This service differs from IntentService in a few minor ways/ It will not + * auto-stop itself after the intent is handled unless the target returns "true" + * in should stop. Since the goal of this service is to handle a single kind of + * intent, it does not queue up batches of intents of the same type. + */ +public abstract class CustomIntentService extends Service { + private String mName; + private boolean mRedelivery; + private volatile ServiceHandler mServiceHandler; + private volatile Looper mServiceLooper; + private static final String LOG_TAG = "CustomIntentService"; + private static final int WHAT_MESSAGE = -10; + + public CustomIntentService(String paramString) { + this.mName = paramString; + } + + @Override + public IBinder onBind(Intent paramIntent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + HandlerThread localHandlerThread = new HandlerThread("IntentService[" + + this.mName + "]"); + localHandlerThread.start(); + this.mServiceLooper = localHandlerThread.getLooper(); + this.mServiceHandler = new ServiceHandler(this.mServiceLooper); + } + + @Override + public void onDestroy() { + Thread localThread = this.mServiceLooper.getThread(); + if ((localThread != null) && (localThread.isAlive())) { + localThread.interrupt(); + } + this.mServiceLooper.quit(); + Log.d(LOG_TAG, "onDestroy"); + } + + protected abstract void onHandleIntent(Intent paramIntent); + + protected abstract boolean shouldStop(); + + @Override + public void onStart(Intent paramIntent, int startId) { + if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { + Message localMessage = this.mServiceHandler.obtainMessage(); + localMessage.arg1 = startId; + localMessage.obj = paramIntent; + localMessage.what = WHAT_MESSAGE; + this.mServiceHandler.sendMessage(localMessage); + } + } + + @Override + public int onStartCommand(Intent paramIntent, int flags, int startId) { + onStart(paramIntent, startId); + return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; + } + + public void setIntentRedelivery(boolean enabled) { + this.mRedelivery = enabled; + } + + private final class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message paramMessage) { + CustomIntentService.this + .onHandleIntent((Intent) paramMessage.obj); + if (shouldStop()) { + Log.d(LOG_TAG, "stopSelf"); + CustomIntentService.this.stopSelf(paramMessage.arg1); + Log.d(LOG_TAG, "afterStopSelf"); + } + } + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java new file mode 100644 index 0000000000..45111b16a3 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java @@ -0,0 +1,92 @@ +/* + * 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.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; + +import android.util.Log; + +/** + * Representation of information about an individual download from the database. + */ +public class DownloadInfo { + public String mUri; + public final int mIndex; + public final String mFileName; + public String mETag; + public long mTotalBytes; + public long mCurrentBytes; + public long mLastMod; + public int mStatus; + public int mControl; + public int mNumFailed; + public int mRetryAfter; + public int mRedirectCount; + + boolean mInitialized; + + public int mFuzz; + + public DownloadInfo(int index, String fileName, String pkg) { + mFuzz = Helpers.sRandom.nextInt(1001); + mFileName = fileName; + mIndex = index; + } + + public void resetDownload() { + mCurrentBytes = 0; + mETag = ""; + mLastMod = 0; + mStatus = 0; + mControl = 0; + mNumFailed = 0; + mRetryAfter = 0; + mRedirectCount = 0; + } + + /** + * Returns the time when a download should be restarted. + */ + public long restartTime(long now) { + if (mNumFailed == 0) { + return now; + } + if (mRetryAfter > 0) { + return mLastMod + mRetryAfter; + } + return mLastMod + + Constants.RETRY_FIRST_DELAY * + (1000 + mFuzz) * (1 << (mNumFailed - 1)); + } + + public void logVerboseInfo() { + Log.v(Constants.TAG, "Service adding new entry"); + Log.v(Constants.TAG, "FILENAME: " + mFileName); + Log.v(Constants.TAG, "URI : " + mUri); + Log.v(Constants.TAG, "FILENAME: " + mFileName); + Log.v(Constants.TAG, "CONTROL : " + mControl); + Log.v(Constants.TAG, "STATUS : " + mStatus); + Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); + Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); + Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); + Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); + Log.v(Constants.TAG, "TOTAL : " + mTotalBytes); + Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); + Log.v(Constants.TAG, "ETAG : " + mETag); + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java new file mode 100644 index 0000000000..0abaf2e052 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java @@ -0,0 +1,229 @@ +/* + * 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; + +// -- GODOT start -- +//import com.android.vending.expansion.downloader.R; +import org.godotengine.godot.R; +// -- GODOT end -- + +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build; +import android.os.Messenger; +import android.support.v4.app.NotificationCompat; + +/** + * This class handles displaying the notification associated with the download + * queue going on in the download manager. It handles multiple status types; + * Some require user interaction and some do not. Some of the user interactions + * may be transient. (for example: the user is queried to continue the download + * on 3G when it started on WiFi, but then the phone locks onto WiFi again so + * the prompt automatically goes away) + * <p/> + * The application interface for the downloader also needs to understand and + * handle these transient states. + */ +public class DownloadNotification implements IDownloaderClient { + + private int mState; + private final Context mContext; + private final NotificationManager mNotificationManager; + private CharSequence mCurrentTitle; + + private IDownloaderClient mClientProxy; + private NotificationCompat.Builder mActiveDownloadBuilder; + private NotificationCompat.Builder mBuilder; + private NotificationCompat.Builder mCurrentBuilder; + private CharSequence mLabel; + private String mCurrentText; + private DownloadProgressInfo mProgressInfo; + private PendingIntent mContentIntent; + + static final String LOGTAG = "DownloadNotification"; + static final int NOTIFICATION_ID = LOGTAG.hashCode(); + + public PendingIntent getClientIntent() { + return mContentIntent; + } + + public void setClientIntent(PendingIntent clientIntent) { + this.mBuilder.setContentIntent(clientIntent); + this.mActiveDownloadBuilder.setContentIntent(clientIntent); + this.mContentIntent = clientIntent; + } + + public void resendState() { + if (null != mClientProxy) { + mClientProxy.onDownloadStateChanged(mState); + } + } + + @Override + public void onDownloadStateChanged(int newState) { + if (null != mClientProxy) { + mClientProxy.onDownloadStateChanged(newState); + } + if (newState != mState) { + mState = newState; + if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) { + return; + } + int stringDownloadID; + int iconResource; + boolean ongoingEvent; + + // get the new title string and paused text + switch (newState) { + case 0: + iconResource = android.R.drawable.stat_sys_warning; + stringDownloadID = R.string.state_unknown; + ongoingEvent = false; + break; + + case IDownloaderClient.STATE_DOWNLOADING: + iconResource = android.R.drawable.stat_sys_download; + stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); + ongoingEvent = true; + break; + + case IDownloaderClient.STATE_FETCHING_URL: + case IDownloaderClient.STATE_CONNECTING: + iconResource = android.R.drawable.stat_sys_download_done; + stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); + ongoingEvent = true; + break; + + case IDownloaderClient.STATE_COMPLETED: + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + iconResource = android.R.drawable.stat_sys_download_done; + stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); + ongoingEvent = false; + break; + + case IDownloaderClient.STATE_FAILED: + case IDownloaderClient.STATE_FAILED_CANCELED: + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + case IDownloaderClient.STATE_FAILED_SDCARD_FULL: + case IDownloaderClient.STATE_FAILED_UNLICENSED: + iconResource = android.R.drawable.stat_sys_warning; + stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); + ongoingEvent = false; + break; + + default: + iconResource = android.R.drawable.stat_sys_warning; + stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); + ongoingEvent = true; + break; + } + + mCurrentText = mContext.getString(stringDownloadID); + mCurrentTitle = mLabel; + mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText); + mCurrentBuilder.setSmallIcon(iconResource); + mCurrentBuilder.setContentTitle(mCurrentTitle); + mCurrentBuilder.setContentText(mCurrentText); + if (ongoingEvent) { + mCurrentBuilder.setOngoing(true); + } else { + mCurrentBuilder.setOngoing(false); + mCurrentBuilder.setAutoCancel(true); + } + mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); + } + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + mProgressInfo = progress; + if (null != mClientProxy) { + mClientProxy.onDownloadProgress(progress); + } + if (progress.mOverallTotal <= 0) { + // we just show the text + mBuilder.setTicker(mCurrentTitle); + mBuilder.setSmallIcon(android.R.drawable.stat_sys_download); + mBuilder.setContentTitle(mCurrentTitle); + mBuilder.setContentText(mCurrentText); + mCurrentBuilder = mBuilder; + } else { + mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false); + mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); + mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download); + mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText); + mActiveDownloadBuilder.setContentTitle(mLabel); + mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification, + Helpers.getTimeRemaining(progress.mTimeRemaining))); + mCurrentBuilder = mActiveDownloadBuilder; + } + mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); + } + + /** + * Called in response to onClientUpdated. Creates a new proxy and notifies + * it of the current state. + * + * @param msg the client Messenger to notify + */ + public void setMessenger(Messenger msg) { + mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); + if (null != mProgressInfo) { + mClientProxy.onDownloadProgress(mProgressInfo); + } + if (mState != -1) { + mClientProxy.onDownloadStateChanged(mState); + } + } + + /** + * Constructor + * + * @param ctx The context to use to obtain access to the Notification + * Service + */ + DownloadNotification(Context ctx, CharSequence applicationLabel) { + mState = -1; + mContext = ctx; + mLabel = applicationLabel; + mNotificationManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mActiveDownloadBuilder = new NotificationCompat.Builder(ctx); + mBuilder = new NotificationCompat.Builder(ctx); + + // Set Notification category and priorities to something that makes sense for a long + // lived background task. + mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); + + mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); + + mCurrentBuilder = mBuilder; + } + + @Override + public void onServiceConnected(Messenger m) { + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java new file mode 100644 index 0000000000..c114b8a64a --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; + +import android.content.Context; +import android.os.PowerManager; +import android.os.Process; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SyncFailedException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; + +/** + * Runs an actual download + */ +public class DownloadThread { + + private Context mContext; + private DownloadInfo mInfo; + private DownloaderService mService; + private final DownloadsDB mDB; + private final DownloadNotification mNotification; + private String mUserAgent; + + public DownloadThread(DownloadInfo info, DownloaderService service, + DownloadNotification notification) { + mContext = service; + mInfo = info; + mService = service; + mNotification = notification; + mDB = DownloadsDB.getDB(service); + mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" + + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" + + android.os.Build.ID + ")" + + service.getPackageName(); + } + + /** + * Returns the default user agent + */ + private String userAgent() { + return mUserAgent; + } + + /** + * State for the entire run() method. + */ + private static class State { + public String mFilename; + public FileOutputStream mStream; + public boolean mCountRetry = false; + public int mRetryAfter = 0; + public int mRedirectCount = 0; + public String mNewUri; + public boolean mGotData = false; + public String mRequestUri; + + public State(DownloadInfo info, DownloaderService service) { + mRedirectCount = info.mRedirectCount; + mRequestUri = info.mUri; + mFilename = service.generateTempSaveFileName(info.mFileName); + } + } + + /** + * State within executeDownload() + */ + private static class InnerState { + public int mBytesSoFar = 0; + public int mBytesThisSession = 0; + public String mHeaderETag; + public boolean mContinuingDownload = false; + public String mHeaderContentLength; + public String mHeaderContentDisposition; + public String mHeaderContentLocation; + public int mBytesNotified = 0; + public long mTimeLastNotification = 0; + } + + /** + * Raised from methods called by run() to indicate that the current request + * should be stopped immediately. Note the message passed to this exception + * will be logged and therefore must be guaranteed not to contain any PII, + * meaning it generally can't include any information about the request URI, + * headers, or destination filename. + */ + private class StopRequest extends Throwable { + + private static final long serialVersionUID = 6338592678988347973L; + public int mFinalStatus; + + public StopRequest(int finalStatus, String message) { + super(message); + mFinalStatus = finalStatus; + } + + public StopRequest(int finalStatus, String message, Throwable throwable) { + super(message, throwable); + mFinalStatus = finalStatus; + } + } + + /** + * Raised from methods called by executeDownload() to indicate that the + * download should be retried immediately. + */ + private class RetryDownload extends Throwable { + + private static final long serialVersionUID = 6196036036517540229L; + } + + /** + * Executes the download in a separate thread + */ + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + State state = new State(mInfo, mService); + PowerManager.WakeLock wakeLock = null; + int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; + + try { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + // -- GODOT start -- + //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); + //wakeLock.acquire(); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); + wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); + // -- GODOT end -- + + if (Constants.LOGV) { + Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); + Log.v(Constants.TAG, " at " + mInfo.mUri); + } + + boolean finished = false; + while (!finished) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); + Log.v(Constants.TAG, " at " + mInfo.mUri); + } + // Set or unset proxy, which may have changed since last GET + // request. + // setDefaultProxy() supports null as proxy parameter. + URL url = new URL(state.mRequestUri); + HttpURLConnection request = (HttpURLConnection)url.openConnection(); + request.setRequestProperty("User-Agent", userAgent()); + try { + executeDownload(state, request); + finished = true; + } catch (RetryDownload exc) { + // fall through + } finally { + request.disconnect(); + request = null; + } + } + + if (Constants.LOGV) { + Log.v(Constants.TAG, "download completed for " + mInfo.mFileName); + Log.v(Constants.TAG, " at " + mInfo.mUri); + } + finalizeDestinationFile(state); + finalStatus = DownloaderService.STATUS_SUCCESS; + } catch (StopRequest error) { + // remove the cause before printing, in case it contains PII + Log.w(Constants.TAG, + "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage()); + error.printStackTrace(); + finalStatus = error.mFinalStatus; + // fall through to finally block + } catch (Throwable ex) { // sometimes the socket code throws unchecked + // exceptions + Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex); + finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; + // falls through to the code that reports an error + } finally { + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + cleanupDestination(state, finalStatus); + notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, + state.mRedirectCount, state.mGotData, state.mFilename); + } + } + + /** + * Fully execute a single download request - setup and send the request, + * handle the response, and transfer the data to the destination file. + */ + private void executeDownload(State state, HttpURLConnection request) + throws StopRequest, RetryDownload { + InnerState innerState = new InnerState(); + byte data[] = new byte[Constants.BUFFER_SIZE]; + + checkPausedOrCanceled(state); + + setupDestinationFile(state, innerState); + addRequestHeaders(innerState, request); + + // check just before sending the request to avoid using an invalid + // connection at all + checkConnectivity(state); + + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING); + int responseCode = sendRequest(state, request); + handleExceptionalStatus(state, innerState, request, responseCode); + + if (Constants.LOGV) { + Log.v(Constants.TAG, "received response for " + mInfo.mUri); + } + + processResponseHeaders(state, innerState, request); + InputStream entityStream = openResponseEntity(state, request); + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING); + transferData(state, innerState, data, entityStream); + } + + /** + * Check if current connectivity is valid for this request. + */ + private void checkConnectivity(State state) throws StopRequest { + switch (mService.getNetworkAvailabilityState(mDB)) { + case DownloaderService.NETWORK_OK: + return; + case DownloaderService.NETWORK_NO_CONNECTION: + throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, + "waiting for network to return"); + case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: + throw new StopRequest( + DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION, + "waiting for wifi or for download over cellular to be authorized"); + case DownloaderService.NETWORK_CANNOT_USE_ROAMING: + throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, + "roaming is not allowed"); + case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE: + throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi"); + } + } + + /** + * Transfer as much data as possible from the HTTP response to the + * destination file. + * + * @param data buffer to use to read data + * @param entityStream stream for reading the HTTP response entity + */ + private void transferData(State state, InnerState innerState, byte[] data, + InputStream entityStream) throws StopRequest { + for (;;) { + int bytesRead = readFromResponse(state, innerState, data, entityStream); + if (bytesRead == -1) { // success, end of stream already reached + handleEndOfStream(state, innerState); + return; + } + + state.mGotData = true; + writeDataToDestination(state, data, bytesRead); + innerState.mBytesSoFar += bytesRead; + innerState.mBytesThisSession += bytesRead; + reportProgress(state, innerState); + + checkPausedOrCanceled(state); + } + } + + /** + * Called after a successful completion to take any necessary action on the + * downloaded file. + */ + private void finalizeDestinationFile(State state) throws StopRequest { + syncDestination(state); + String tempFilename = state.mFilename; + String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName); + if (!state.mFilename.equals(finalFilename)) { + File startFile = new File(tempFilename); + File destFile = new File(finalFilename); + if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) { + if (!startFile.renameTo(destFile)) { + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "unable to finalize destination file"); + } + } else { + throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, + "file delivered with incorrect size. probably due to network not browser configured"); + } + } + } + + /** + * Called just before the thread finishes, regardless of status, to take any + * necessary action on the downloaded file. + */ + private void cleanupDestination(State state, int finalStatus) { + closeDestination(state); + if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) { + new File(state.mFilename).delete(); + state.mFilename = null; + } + } + + /** + * Sync the destination file to storage. + */ + private void syncDestination(State state) { + FileOutputStream downloadedFileStream = null; + try { + downloadedFileStream = new FileOutputStream(state.mFilename, true); + downloadedFileStream.getFD().sync(); + } catch (FileNotFoundException ex) { + Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); + } catch (SyncFailedException ex) { + Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); + } catch (IOException ex) { + Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); + } catch (RuntimeException ex) { + Log.w(Constants.TAG, "exception while syncing file: ", ex); + } finally { + if (downloadedFileStream != null) { + try { + downloadedFileStream.close(); + } catch (IOException ex) { + Log.w(Constants.TAG, "IOException while closing synced file: ", ex); + } catch (RuntimeException ex) { + Log.w(Constants.TAG, "exception while closing file: ", ex); + } + } + } + } + + /** + * Close the destination output stream. + */ + private void closeDestination(State state) { + try { + // close the file + if (state.mStream != null) { + state.mStream.close(); + state.mStream = null; + } + } catch (IOException ex) { + if (Constants.LOGV) { + Log.v(Constants.TAG, "exception when closing the file after download : " + ex); + } + // nothing can really be done if the file can't be closed + } + } + + /** + * Check if the download has been paused or canceled, stopping the request + * appropriately if it has been. + */ + private void checkPausedOrCanceled(State state) throws StopRequest { + if (mService.getControl() == DownloaderService.CONTROL_PAUSED) { + int status = mService.getStatus(); + switch (status) { + case DownloaderService.STATUS_PAUSED_BY_APP: + throw new StopRequest(mService.getStatus(), + "download paused"); + } + } + } + + /** + * Report download progress through the database if necessary. + */ + private void reportProgress(State state, InnerState innerState) { + long now = System.currentTimeMillis(); + if (innerState.mBytesSoFar - innerState.mBytesNotified + > Constants.MIN_PROGRESS_STEP + && now - innerState.mTimeLastNotification + > Constants.MIN_PROGRESS_TIME) { + // we store progress updates to the database here + mInfo.mCurrentBytes = innerState.mBytesSoFar; + mDB.updateDownloadCurrentBytes(mInfo); + + innerState.mBytesNotified = innerState.mBytesSoFar; + innerState.mTimeLastNotification = now; + + long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar; + + if (Constants.LOGVV) { + Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " + + mInfo.mTotalBytes); + Log.v(Constants.TAG, " total " + totalBytesSoFar + " out of " + + mService.mTotalLength); + } + + mService.notifyUpdateBytes(totalBytesSoFar); + } + } + + /** + * Write a data buffer to the destination file. + * + * @param data buffer containing the data to write + * @param bytesRead how many bytes to write from the buffer + */ + private void writeDataToDestination(State state, byte[] data, int bytesRead) + throws StopRequest { + for (;;) { + try { + if (state.mStream == null) { + state.mStream = new FileOutputStream(state.mFilename, true); + } + state.mStream.write(data, 0, bytesRead); + // we close after every write --- this may be too inefficient + closeDestination(state); + return; + } catch (IOException ex) { + if (!Helpers.isExternalMediaMounted()) { + throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR, + "external media not mounted while writing destination file"); + } + + long availableBytes = + Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename)); + if (availableBytes < bytesRead) { + throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR, + "insufficient space while writing destination file", ex); + } + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "while writing destination file: " + ex.toString(), ex); + } + } + } + + /** + * Called when we've reached the end of the HTTP response stream, to update + * the database and check for consistency. + */ + private void handleEndOfStream(State state, InnerState innerState) throws StopRequest { + mInfo.mCurrentBytes = innerState.mBytesSoFar; + // this should always be set from the market + // if ( innerState.mHeaderContentLength == null ) { + // mInfo.mTotalBytes = innerState.mBytesSoFar; + // } + mDB.updateDownload(mInfo); + + boolean lengthMismatched = (innerState.mHeaderContentLength != null) + && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength)); + if (lengthMismatched) { + if (cannotResume(innerState)) { + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + "mismatched content length"); + } else { + throw new StopRequest(getFinalStatusForHttpError(state), + "closed socket before end of file"); + } + } + } + + private boolean cannotResume(InnerState innerState) { + return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null; + } + + /** + * Read some data from the HTTP response stream, handling I/O errors. + * + * @param data buffer to use to read data + * @param entityStream stream for reading the HTTP response entity + * @return the number of bytes actually read or -1 if the end of the stream + * has been reached + */ + private int readFromResponse(State state, InnerState innerState, byte[] data, + InputStream entityStream) throws StopRequest { + try { + return entityStream.read(data); + } catch (IOException ex) { + logNetworkState(); + mInfo.mCurrentBytes = innerState.mBytesSoFar; + mDB.updateDownload(mInfo); + if (cannotResume(innerState)) { + String message = "while reading response: " + ex.toString() + + ", can't resume interrupted download with no ETag"; + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + message, ex); + } else { + throw new StopRequest(getFinalStatusForHttpError(state), + "while reading response: " + ex.toString(), ex); + } + } + } + + /** + * Open a stream for the HTTP response entity, handling I/O errors. + * + * @return an InputStream to read the response entity + */ + private InputStream openResponseEntity(State state, HttpURLConnection response) + throws StopRequest { + try { + return response.getInputStream(); + } catch (IOException ex) { + logNetworkState(); + throw new StopRequest(getFinalStatusForHttpError(state), + "while getting entity: " + ex.toString(), ex); + } + } + + private void logNetworkState() { + if (Constants.LOGX) { + Log.i(Constants.TAG, + "Net " + + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up" + : "Down")); + } + } + + /** + * Read HTTP response headers and take appropriate action, including setting + * up the destination file and updating the database. + */ + private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response) + throws StopRequest { + if (innerState.mContinuingDownload) { + // ignore response headers on resume requests + return; + } + + readResponseHeaders(state, innerState, response); + + try { + state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes); + } catch (DownloaderService.GenerateSaveFileError exc) { + throw new StopRequest(exc.mStatus, exc.mMessage); + } + try { + state.mStream = new FileOutputStream(state.mFilename); + } catch (FileNotFoundException exc) { + // make sure the directory exists + File pathFile = new File(Helpers.getSaveFilePath(mService)); + try { + if (pathFile.mkdirs()) { + state.mStream = new FileOutputStream(state.mFilename); + } + } catch (Exception ex) { + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "while opening destination file: " + exc.toString(), exc); + } + } + if (Constants.LOGV) { + Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); + } + + updateDatabaseFromHeaders(state, innerState); + // check connectivity again now that we know the total size + checkConnectivity(state); + } + + /** + * Update necessary database fields based on values of HTTP response headers + * that have been read. + */ + private void updateDatabaseFromHeaders(State state, InnerState innerState) { + mInfo.mETag = innerState.mHeaderETag; + mDB.updateDownload(mInfo); + } + + /** + * Read headers from the HTTP response and store them into local state. + */ + private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response) + throws StopRequest { + String value = response.getHeaderField("Content-Disposition"); + if (value != null) { + innerState.mHeaderContentDisposition = value; + } + value = response.getHeaderField("Content-Location"); + if (value != null) { + innerState.mHeaderContentLocation = value; + } + value = response.getHeaderField("ETag"); + if (value != null) { + innerState.mHeaderETag = value; + } + String headerTransferEncoding = null; + value = response.getHeaderField("Transfer-Encoding"); + if (value != null) { + headerTransferEncoding = value; + } + String headerContentType = null; + value = response.getHeaderField("Content-Type"); + if (value != null) { + headerContentType = value; + if (!headerContentType.equals("application/vnd.android.obb")) { + throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, + "file delivered with incorrect Mime type"); + } + } + + if (headerTransferEncoding == null) { + long contentLength = response.getContentLength(); + if (value != null) { + // this is always set from Market + if (contentLength != -1 && contentLength != mInfo.mTotalBytes) { + // we're most likely on a bad wifi connection -- we should + // probably + // also look at the mime type --- but the size mismatch is + // enough + // to tell us that something is wrong here + Log.e(Constants.TAG, "Incorrect file size delivered."); + } else { + innerState.mHeaderContentLength = Long.toString(contentLength); + } + } + } else { + // Ignore content-length with transfer-encoding - 2616 4.4 3 + if (Constants.LOGVV) { + Log.v(Constants.TAG, + "ignoring content-length because of xfer-encoding"); + } + } + if (Constants.LOGVV) { + Log.v(Constants.TAG, "Content-Disposition: " + + innerState.mHeaderContentDisposition); + Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); + Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); + Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag); + Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); + } + + boolean noSizeInfo = innerState.mHeaderContentLength == null + && (headerTransferEncoding == null + || !headerTransferEncoding.equalsIgnoreCase("chunked")); + if (noSizeInfo) { + throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, + "can't know size of download, giving up"); + } + } + + /** + * Check the HTTP response status and handle anything unusual (e.g. not + * 200/206). + */ + private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode) + throws StopRequest, RetryDownload { + if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { + handleServiceUnavailable(state, connection); + } + int expectedStatus = innerState.mContinuingDownload ? 206 + : DownloaderService.STATUS_SUCCESS; + if (responseCode != expectedStatus) { + handleOtherStatus(state, innerState, responseCode); + } else { + // no longer redirected + state.mRedirectCount = 0; + } + } + + /** + * Handle a status that we don't know how to deal with properly. + */ + private void handleOtherStatus(State state, InnerState innerState, int statusCode) + throws StopRequest { + int finalStatus; + if (DownloaderService.isStatusError(statusCode)) { + finalStatus = statusCode; + } else if (statusCode >= 300 && statusCode < 400) { + finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT; + } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) { + finalStatus = DownloaderService.STATUS_CANNOT_RESUME; + } else { + finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE; + } + throw new StopRequest(finalStatus, "http error " + statusCode); + } + + /** + * Add headers for this download to the HTTP request to allow for resume. + */ + private void addRequestHeaders(InnerState innerState, HttpURLConnection request) { + if (innerState.mContinuingDownload) { + if (innerState.mHeaderETag != null) { + request.setRequestProperty("If-Match", innerState.mHeaderETag); + } + request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-"); + } + } + + /** + * Handle a 503 Service Unavailable status by processing the Retry-After + * header. + */ + private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "got HTTP response code 503"); + } + state.mCountRetry = true; + String retryAfterValue = connection.getHeaderField("Retry-After"); + if (retryAfterValue != null) { + try { + if (Constants.LOGVV) { + Log.v(Constants.TAG, "Retry-After :" + retryAfterValue); + } + state.mRetryAfter = Integer.parseInt(retryAfterValue); + if (state.mRetryAfter < 0) { + state.mRetryAfter = 0; + } else { + if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { + state.mRetryAfter = Constants.MIN_RETRY_AFTER; + } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { + state.mRetryAfter = Constants.MAX_RETRY_AFTER; + } + state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); + state.mRetryAfter *= 1000; + } + } catch (NumberFormatException ex) { + // ignored - retryAfter stays 0 in this case. + } + } + throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY, + "got 503 Service Unavailable, will retry later"); + } + + /** + * Send the request to the server, handling any I/O exceptions. + */ + private int sendRequest(State state, HttpURLConnection request) + throws StopRequest { + try { + return request.getResponseCode(); + } catch (IllegalArgumentException ex) { + throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, + "while trying to execute request: " + ex.toString(), ex); + } catch (IOException ex) { + logNetworkState(); + throw new StopRequest(getFinalStatusForHttpError(state), + "while trying to execute request: " + ex.toString(), ex); + } + } + + private int getFinalStatusForHttpError(State state) { + if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) { + return DownloaderService.STATUS_WAITING_FOR_NETWORK; + } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { + state.mCountRetry = true; + return DownloaderService.STATUS_WAITING_TO_RETRY; + } else { + Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed); + return DownloaderService.STATUS_HTTP_DATA_ERROR; + } + } + + /** + * Prepare the destination file to receive data. If the file already exists, + * we'll set up appropriately for resumption. + */ + private void setupDestinationFile(State state, InnerState innerState) + throws StopRequest { + if (state.mFilename != null) { // only true if we've already run a + // thread for this download + if (!Helpers.isFilenameValid(state.mFilename)) { + // this should never happen + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "found invalid internal destination filename"); + } + // We're resuming a download that got interrupted + File f = new File(state.mFilename); + if (f.exists()) { + long fileLength = f.length(); + if (fileLength == 0) { + // The download hadn't actually started, we can restart from + // scratch + f.delete(); + state.mFilename = null; + } else if (mInfo.mETag == null) { + // This should've been caught upon failure + f.delete(); + throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, + "Trying to resume a download that can't be resumed"); + } else { + // All right, we'll be able to resume this download + try { + state.mStream = new FileOutputStream(state.mFilename, true); + } catch (FileNotFoundException exc) { + throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, + "while opening destination for resuming: " + exc.toString(), exc); + } + innerState.mBytesSoFar = (int) fileLength; + if (mInfo.mTotalBytes != -1) { + innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); + } + innerState.mHeaderETag = mInfo.mETag; + innerState.mContinuingDownload = true; + } + } + } + + if (state.mStream != null) { + closeDestination(state); + } + } + + /** + * Stores information about the completed download, and notifies the + * initiating application. + */ + private void notifyDownloadCompleted( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename) { + updateDownloadDatabase( + status, countRetry, retryAfter, redirectCount, gotData, filename); + if (DownloaderService.isStatusCompleted(status)) { + // TBD: send status update? + } + } + + private void updateDownloadDatabase( + int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, + String filename) { + mInfo.mStatus = status; + mInfo.mRetryAfter = retryAfter; + mInfo.mRedirectCount = redirectCount; + mInfo.mLastMod = System.currentTimeMillis(); + if (!countRetry) { + mInfo.mNumFailed = 0; + } else if (gotData) { + mInfo.mNumFailed = 1; + } else { + mInfo.mNumFailed++; + } + mDB.updateDownload(mInfo); + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java new file mode 100644 index 0000000000..8d41a76900 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java @@ -0,0 +1,1346 @@ +/* + * 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.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; +import com.google.android.vending.expansion.downloader.IDownloaderService; +import com.google.android.vending.expansion.downloader.IStub; +import com.google.android.vending.licensing.AESObfuscator; +import com.google.android.vending.licensing.APKExpansionPolicy; +import com.google.android.vending.licensing.LicenseChecker; +import com.google.android.vending.licensing.LicenseCheckerCallback; +import com.google.android.vending.licensing.Policy; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Messenger; +import android.os.SystemClock; +import android.provider.Settings.Secure; +import android.telephony.TelephonyManager; +import android.util.Log; + +// -- GODOT start -- +import android.annotation.SuppressLint; +// -- GODOT end -- + +import java.io.File; + +/** + * Performs the background downloads requested by applications that use the + * Downloads provider. This service does not run as a foreground task, so + * Android may kill it off at will, but it will try to restart itself if it can. + * Note that Android by default will kill off any process that has an open file + * handle on the shared (SD Card) partition if the partition is unmounted. + */ +public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { + + public DownloaderService() { + super("LVLDownloadService"); + } + + private static final String LOG_TAG = "LVLDL"; + + // the following NETWORK_* constants are used to indicates specific reasons + // for disallowing a + // download from using a network, since specific causes can require special + // handling + + /** + * The network is usable for the given download. + */ + public static final int NETWORK_OK = 1; + + /** + * There is no network connectivity. + */ + public static final int NETWORK_NO_CONNECTION = 2; + + /** + * The download exceeds the maximum size for this network. + */ + public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; + + /** + * The download exceeds the recommended maximum size for this network, the + * user must confirm for this download to proceed without WiFi. + */ + public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; + + /** + * The current connection is roaming, and the download can't proceed over a + * roaming connection. + */ + public static final int NETWORK_CANNOT_USE_ROAMING = 5; + + /** + * The app requesting the download specific that it can't use the current + * network connection. + */ + public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; + + /** + * For intents used to notify the user that a download exceeds a size + * threshold, if this extra is true, WiFi is required for this download + * size; otherwise, it is only recommended. + */ + public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; + public static final String EXTRA_FILE_NAME = "downloadId"; + + /** + * Used with DOWNLOAD_STATUS + */ + public static final String EXTRA_STATUS_STATE = "ESS"; + public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS"; + public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS"; + public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP"; + public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP"; + + public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged"; + + /** + * Broadcast intent action sent by the download manager when a download + * completes. + */ + public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE"; + + /** + * Broadcast intent action sent by the download manager when download status + * changes. + */ + public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS"; + + /* + * Lists the states that the download manager can set on a download to + * notify applications of the download progress. The codes follow the HTTP + * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not + * used by the download manager)<br> 4xx: client errors<br> 5xx: server + * errors + */ + + /** + * Returns whether the status is informational (i.e. 1xx). + */ + public static boolean isStatusInformational(int status) { + return (status >= 100 && status < 200); + } + + /** + * Returns whether the status is a success (i.e. 2xx). + */ + public static boolean isStatusSuccess(int status) { + return (status >= 200 && status < 300); + } + + /** + * Returns whether the status is an error (i.e. 4xx or 5xx). + */ + public static boolean isStatusError(int status) { + return (status >= 400 && status < 600); + } + + /** + * Returns whether the status is a client error (i.e. 4xx). + */ + public static boolean isStatusClientError(int status) { + return (status >= 400 && status < 500); + } + + /** + * Returns whether the status is a server error (i.e. 5xx). + */ + public static boolean isStatusServerError(int status) { + return (status >= 500 && status < 600); + } + + /** + * Returns whether the download has completed (either with success or + * error). + */ + public static boolean isStatusCompleted(int status) { + return (status >= 200 && status < 300) + || (status >= 400 && status < 600); + } + + /** + * This download hasn't stated yet + */ + public static final int STATUS_PENDING = 190; + + /** + * This download has started + */ + public static final int STATUS_RUNNING = 192; + + /** + * This download has been paused by the owning app. + */ + public static final int STATUS_PAUSED_BY_APP = 193; + + /** + * This download encountered some network error and is waiting before + * retrying the request. + */ + public static final int STATUS_WAITING_TO_RETRY = 194; + + /** + * This download is waiting for network connectivity to proceed. + */ + public static final int STATUS_WAITING_FOR_NETWORK = 195; + + /** + * This download is waiting for a Wi-Fi connection to proceed or for + * permission to download over cellular. + */ + public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196; + + /** + * This download is waiting for a Wi-Fi connection to proceed. + */ + public static final int STATUS_QUEUED_FOR_WIFI = 197; + + /** + * This download has successfully completed. Warning: there might be other + * status values that indicate success in the future. Use isSucccess() to + * capture the entire category. + * + * @hide + */ + public static final int STATUS_SUCCESS = 200; + + /** + * The requested URL is no longer available + */ + public static final int STATUS_FORBIDDEN = 403; + + /** + * The file was delivered incorrectly + */ + public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487; + + /** + * The requested destination file already exists. + */ + public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + + /** + * Some possibly transient error occurred, but we can't resume the download. + */ + public static final int STATUS_CANNOT_RESUME = 489; + + /** + * This download was canceled + * + * @hide + */ + public static final int STATUS_CANCELED = 490; + + /** + * This download has completed with an error. Warning: there will be other + * status values that indicate errors in the future. Use isStatusError() to + * capture the entire category. + */ + public static final int STATUS_UNKNOWN_ERROR = 491; + + /** + * This download couldn't be completed because of a storage issue. + * Typically, that's because the filesystem is missing or full. Use the more + * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and + * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. + * + * @hide + */ + public static final int STATUS_FILE_ERROR = 492; + + /** + * This download couldn't be completed because of an HTTP redirect response + * that the download manager couldn't handle. + * + * @hide + */ + public static final int STATUS_UNHANDLED_REDIRECT = 493; + + /** + * This download couldn't be completed because of an unspecified unhandled + * HTTP code. + * + * @hide + */ + public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + + /** + * This download couldn't be completed because of an error receiving or + * processing data at the HTTP level. + * + * @hide + */ + public static final int STATUS_HTTP_DATA_ERROR = 495; + + /** + * This download couldn't be completed because of an HttpException while + * setting up the request. + * + * @hide + */ + public static final int STATUS_HTTP_EXCEPTION = 496; + + /** + * This download couldn't be completed because there were too many + * redirects. + * + * @hide + */ + public static final int STATUS_TOO_MANY_REDIRECTS = 497; + + /** + * This download couldn't be completed due to insufficient storage space. + * Typically, this is because the SD card is full. + * + * @hide + */ + public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + + /** + * This download couldn't be completed because no external storage device + * was found. Typically, this is because the SD card is not mounted. + * + * @hide + */ + public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + + /** + * This download is allowed to run. + * + * @hide + */ + public static final int CONTROL_RUN = 0; + + /** + * This download must pause at the first opportunity. + * + * @hide + */ + public static final int CONTROL_PAUSED = 1; + + /** + * This download is visible but only shows in the notifications while it's + * in progress. + * + * @hide + */ + public static final int VISIBILITY_VISIBLE = 0; + + /** + * This download is visible and shows in the notifications while in progress + * and after completion. + * + * @hide + */ + public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; + + /** + * This download doesn't show in the UI or in the notifications. + * + * @hide + */ + public static final int VISIBILITY_HIDDEN = 2; + + /** + * Bit flag for setAllowedNetworkTypes corresponding to + * {@link ConnectivityManager#TYPE_MOBILE}. + */ + public static final int NETWORK_MOBILE = 1 << 0; + + /** + * Bit flag for setAllowedNetworkTypes corresponding to + * {@link ConnectivityManager#TYPE_WIFI}. + */ + public static final int NETWORK_WIFI = 1 << 1; + + private final static String TEMP_EXT = ".tmp"; + + /** + * Service thread status + */ + private static boolean sIsRunning; + + @Override + public IBinder onBind(Intent paramIntent) { + Log.d(Constants.TAG, "Service Bound"); + return this.mServiceMessenger.getBinder(); + } + + /** + * Network state. + */ + private boolean mIsConnected; + private boolean mIsFailover; + private boolean mIsCellularConnection; + private boolean mIsRoaming; + private boolean mIsAtLeast3G; + private boolean mIsAtLeast4G; + private boolean mStateChanged; + + /** + * Download state + */ + private int mControl; + private int mStatus; + + public boolean isWiFi() { + return mIsConnected && !mIsCellularConnection; + } + + /** + * Bindings to important services + */ + private ConnectivityManager mConnectivityManager; + private WifiManager mWifiManager; + + /** + * Package we are downloading for (defaults to package of application) + */ + private PackageInfo mPackageInfo; + + /** + * Byte counts + */ + long mBytesSoFar; + long mTotalLength; + int mFileCount; + + /** + * Used for calculating time remaining and speed + */ + long mBytesAtSample; + long mMillisecondsAtSample; + float mAverageDownloadSpeed; + + /** + * Our binding to the network state broadcasts + */ + private BroadcastReceiver mConnReceiver; + final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); + final private Messenger mServiceMessenger = mServiceStub.getMessenger(); + private Messenger mClientMessenger; + private DownloadNotification mNotification; + private PendingIntent mPendingIntent; + private PendingIntent mAlarmIntent; + + /** + * Updates the network type based upon the type and subtype returned from + * the connectivity manager. Subtype is only used for cellular signals. + * + * @param type + * @param subType + */ + private void updateNetworkType(int type, int subType) { + switch (type) { + case ConnectivityManager.TYPE_WIFI: + case ConnectivityManager.TYPE_ETHERNET: + case ConnectivityManager.TYPE_BLUETOOTH: + mIsCellularConnection = false; + mIsAtLeast3G = false; + mIsAtLeast4G = false; + break; + case ConnectivityManager.TYPE_WIMAX: + mIsCellularConnection = true; + mIsAtLeast3G = true; + mIsAtLeast4G = true; + break; + case ConnectivityManager.TYPE_MOBILE: + mIsCellularConnection = true; + switch (subType) { + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_IDEN: + mIsAtLeast3G = false; + mIsAtLeast4G = false; + break; + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_UMTS: + mIsAtLeast3G = true; + mIsAtLeast4G = false; + break; + case TelephonyManager.NETWORK_TYPE_LTE: // 4G + case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop + // with 4G + case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but + // marketed as + // 4G + mIsAtLeast3G = true; + mIsAtLeast4G = true; + break; + default: + mIsCellularConnection = false; + mIsAtLeast3G = false; + mIsAtLeast4G = false; + } + } + } + + private void updateNetworkState(NetworkInfo info) { + boolean isConnected = mIsConnected; + boolean isFailover = mIsFailover; + boolean isCellularConnection = mIsCellularConnection; + boolean isRoaming = mIsRoaming; + boolean isAtLeast3G = mIsAtLeast3G; + if (null != info) { + mIsRoaming = info.isRoaming(); + mIsFailover = info.isFailover(); + mIsConnected = info.isConnected(); + updateNetworkType(info.getType(), info.getSubtype()); + } else { + mIsRoaming = false; + mIsFailover = false; + mIsConnected = false; + updateNetworkType(-1, -1); + } + mStateChanged = (mStateChanged || isConnected != mIsConnected + || isFailover != mIsFailover + || isCellularConnection != mIsCellularConnection + || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G); + if (Constants.LOGVV) { + if (mStateChanged) { + Log.v(LOG_TAG, "Network state changed: "); + Log.v(LOG_TAG, "Starting State: " + + (isConnected ? "Connected " : "Not Connected ") + + (isCellularConnection ? "Cellular " : "WiFi ") + + (isRoaming ? "Roaming " : "Local ") + + (isAtLeast3G ? "3G+ " : "<3G ")); + Log.v(LOG_TAG, "Ending State: " + + (mIsConnected ? "Connected " : "Not Connected ") + + (mIsCellularConnection ? "Cellular " : "WiFi ") + + (mIsRoaming ? "Roaming " : "Local ") + + (mIsAtLeast3G ? "3G+ " : "<3G ")); + + if (isServiceRunning()) { + if (mIsRoaming) { + mStatus = STATUS_WAITING_FOR_NETWORK; + mControl = CONTROL_PAUSED; + } else if (mIsCellularConnection) { + DownloadsDB db = DownloadsDB.getDB(this); + int flags = db.getFlags(); + if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { + mStatus = STATUS_QUEUED_FOR_WIFI; + mControl = CONTROL_PAUSED; + } + } + } + + } + } + } + + /** + * Polls the network state, setting the flags appropriately. + */ + void pollNetworkState() { + if (null == mConnectivityManager) { + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + } + if (null == mWifiManager) { + mWifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + if (mConnectivityManager == null) { + Log.w(Constants.TAG, + "couldn't get connectivity manager to poll network state"); + } else { + @SuppressLint("MissingPermission") + NetworkInfo activeInfo = mConnectivityManager + .getActiveNetworkInfo(); + updateNetworkState(activeInfo); + } + } + + public static final int NO_DOWNLOAD_REQUIRED = 0; + public static final int LVL_CHECK_REQUIRED = 1; + public static final int DOWNLOAD_REQUIRED = 2; + + public static final String EXTRA_PACKAGE_NAME = "EPN"; + public static final String EXTRA_PENDING_INTENT = "EPI"; + public static final String EXTRA_MESSAGE_HANDLER = "EMH"; + + /** + * Returns true if the LVL check is required + * + * @param db a downloads DB synchronized with the latest state + * @param pi the package info for the project + * @return returns true if the filenames need to be returned + */ + private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) { + // we need to update the LVL check and get a successful status to + // proceed + if (db.mVersionCode != pi.versionCode) { + return true; + } + return false; + } + + /** + * Careful! Only use this internally. + * + * @return whether we think the service is running + */ + private static synchronized boolean isServiceRunning() { + return sIsRunning; + } + + private static synchronized void setServiceRunning(boolean isRunning) { + sIsRunning = isRunning; + } + + public static int startDownloadServiceIfRequired(Context context, + Intent intent, Class<?> serviceClass) throws NameNotFoundException { + final PendingIntent pendingIntent = (PendingIntent) intent + .getParcelableExtra(EXTRA_PENDING_INTENT); + return startDownloadServiceIfRequired(context, pendingIntent, + serviceClass); + } + + public static int startDownloadServiceIfRequired(Context context, + PendingIntent pendingIntent, Class<?> serviceClass) + throws NameNotFoundException + { + String packageName = context.getPackageName(); + String className = serviceClass.getName(); + + return startDownloadServiceIfRequired(context, pendingIntent, + packageName, className); + } + + /** + * Starts the download if necessary. This function starts a flow that does ` + * many things. 1) Checks to see if the APK version has been checked and the + * metadata database updated 2) If the APK version does not match, checks + * the new LVL status to see if a new download is required 3) If the APK + * version does match, then checks to see if the download(s) have been + * completed 4) If the downloads have been completed, returns + * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the + * startup of an application to quickly ascertain if the application needs + * to wait to hear about any updated APK expansion files. Note that this + * does mean that the application MUST be run for the first time with a + * network connection, even if Market delivers all of the files. + * + * @param context + * @param pendingIntent + * @return true if the app should wait for more guidance from the + * downloader, false if the app can continue + * @throws NameNotFoundException + */ + public static int startDownloadServiceIfRequired(Context context, + PendingIntent pendingIntent, String classPackage, String className) + throws NameNotFoundException { + // first: do we need to do an LVL update? + // we begin by getting our APK version from the package manager + final PackageInfo pi = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + + int status = NO_DOWNLOAD_REQUIRED; + + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(context); + + // we need to update the LVL check and get a successful status to + // proceed + if (isLVLCheckRequired(db, pi)) { + status = LVL_CHECK_REQUIRED; + } + // we don't have to update LVL. do we still have a download to start? + if (db.mStatus == 0) { + DownloadInfo[] infos = db.getDownloads(); + if (null != infos) { + for (DownloadInfo info : infos) { + if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) { + status = DOWNLOAD_REQUIRED; + db.updateStatus(-1); + break; + } + } + } + } else { + status = DOWNLOAD_REQUIRED; + } + switch (status) { + case DOWNLOAD_REQUIRED: + case LVL_CHECK_REQUIRED: + Intent fileIntent = new Intent(); + fileIntent.setClassName(classPackage, className); + fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); + context.startService(fileIntent); + break; + } + return status; + } + + @Override + public void requestAbortDownload() { + mControl = CONTROL_PAUSED; + mStatus = STATUS_CANCELED; + } + + @Override + public void requestPauseDownload() { + mControl = CONTROL_PAUSED; + mStatus = STATUS_PAUSED_BY_APP; + } + + @Override + public void setDownloadFlags(int flags) { + DownloadsDB.getDB(this).updateFlags(flags); + } + + @Override + public void requestContinueDownload() { + if (mControl == CONTROL_PAUSED) { + mControl = CONTROL_RUN; + } + Intent fileIntent = new Intent(this, this.getClass()); + fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + this.startService(fileIntent); + } + + public abstract String getPublicKey(); + + public abstract byte[] getSALT(); + + public abstract String getAlarmReceiverClassName(); + + private class LVLRunnable implements Runnable { + LVLRunnable(Context context, PendingIntent intent) { + mContext = context; + mPendingIntent = intent; + } + + final Context mContext; + + @Override + public void run() { + setServiceRunning(true); + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL); + String deviceId = Secure.getString(mContext.getContentResolver(), + Secure.ANDROID_ID); + + final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, + new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); + + // reset our policy back to the start of the world to force a + // re-check + aep.resetPolicy(); + + // let's try and get the OBB file from LVL first + // Construct the LicenseChecker with a Policy. + final LicenseChecker checker = new LicenseChecker(mContext, aep, + getPublicKey() // Your public licensing key. + ); + checker.checkAccess(new LicenseCheckerCallback() { + + @Override + public void allow(int reason) { + try { + int count = aep.getExpansionURLCount(); + DownloadsDB db = DownloadsDB.getDB(mContext); + int status = 0; + if (count != 0) { + for (int i = 0; i < count; i++) { + String currentFileName = aep + .getExpansionFileName(i); + if (null != currentFileName) { + DownloadInfo di = new DownloadInfo(i, + currentFileName, mContext.getPackageName()); + + long fileSize = aep.getExpansionFileSize(i); + if (handleFileUpdated(db, i, currentFileName, + fileSize)) { + status |= -1; + di.resetDownload(); + di.mUri = aep.getExpansionURL(i); + di.mTotalBytes = fileSize; + di.mStatus = status; + db.updateDownload(di); + } else { + // we need to read the download + // information + // from + // the database + DownloadInfo dbdi = db + .getDownloadInfoByFileName(di.mFileName); + if (null == dbdi) { + // the file exists already and is + // the + // correct size + // was delivered by Market or + // through + // another mechanism + Log.d(LOG_TAG, "file " + di.mFileName + + " found. Not downloading."); + di.mStatus = STATUS_SUCCESS; + di.mTotalBytes = fileSize; + di.mCurrentBytes = fileSize; + di.mUri = aep.getExpansionURL(i); + db.updateDownload(di); + } else if (dbdi.mStatus != STATUS_SUCCESS) { + // we just update the URL + dbdi.mUri = aep.getExpansionURL(i); + db.updateDownload(dbdi); + status |= -1; + } + } + } + } + } + // first: do we need to do an LVL update? + // we begin by getting our APK version from the package + // manager + PackageInfo pi; + try { + pi = mContext.getPackageManager().getPackageInfo( + mContext.getPackageName(), 0); + db.updateMetadata(pi.versionCode, status); + Class<?> serviceClass = DownloaderService.this.getClass(); + switch (startDownloadServiceIfRequired(mContext, mPendingIntent, + serviceClass)) { + case NO_DOWNLOAD_REQUIRED: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); + break; + case LVL_CHECK_REQUIRED: + // DANGER WILL ROBINSON! + Log.e(LOG_TAG, "In LVL checking loop!"); + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); + throw new RuntimeException( + "Error with LVL checking and database integrity"); + case DOWNLOAD_REQUIRED: + // do nothing. the download will notify the + // application + // when things are done + break; + } + } catch (NameNotFoundException e1) { + e1.printStackTrace(); + throw new RuntimeException( + "Error with getting information from package name"); + } + } finally { + setServiceRunning(false); + } + } + + @Override + public void dontAllow(int reason) { + try + { + switch (reason) { + case Policy.NOT_LICENSED: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); + break; + case Policy.RETRY: + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); + break; + } + } finally { + setServiceRunning(false); + } + + } + + @Override + public void applicationError(int errorCode) { + try { + mNotification + .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); + } finally { + setServiceRunning(false); + } + } + + }); + + } + + }; + + /** + * Updates the LVL information from the server. + * + * @param context + */ + public void updateLVL(final Context context) { + Context c = context.getApplicationContext(); + Handler h = new Handler(c.getMainLooper()); + h.post(new LVLRunnable(c, mPendingIntent)); + } + + /** + * The APK has been updated and a filename has been sent down from the + * Market call. If the file has the same name as the previous file, we do + * nothing as the file is guaranteed to be the same. If the file does not + * have the same name, we download it if it hasn't already been delivered by + * Market. + * + * @param index the index of the file from market (0 = main, 1 = patch) + * @param filename the name of the new file + * @param fileSize the size of the new file + * @return + */ + public boolean handleFileUpdated(DownloadsDB db, int index, + String filename, long fileSize) { + DownloadInfo di = db.getDownloadInfoByFileName(filename); + if (null != di) { + String oldFile = di.mFileName; + // cleanup + if (null != oldFile) { + if (filename.equals(oldFile)) { + return false; + } + + // remove partially downloaded file if it is there + String deleteFile = Helpers.generateSaveFileName(this, oldFile); + File f = new File(deleteFile); + if (f.exists()) + f.delete(); + } + } + return !Helpers.doesFileExist(this, filename, fileSize, true); + } + + private void scheduleAlarm(long wakeUp) { + AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarms == null) { + Log.e(Constants.TAG, "couldn't get alarm manager"); + return; + } + + if (Constants.LOGV) { + Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); + } + + String className = getAlarmReceiverClassName(); + Intent intent = new Intent(Constants.ACTION_RETRY); + intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + intent.setClassName(this.getPackageName(), + className); + mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, + PendingIntent.FLAG_ONE_SHOT); + alarms.set( + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + wakeUp, mAlarmIntent + ); + } + + private void cancelAlarms() { + if (null != mAlarmIntent) { + AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarms == null) { + Log.e(Constants.TAG, "couldn't get alarm manager"); + return; + } + alarms.cancel(mAlarmIntent); + mAlarmIntent = null; + } + } + + /** + * We use this to track network state, such as when WiFi, Cellular, etc. is + * enabled when downloads are paused or in progress. + */ + private class InnerBroadcastReceiver extends BroadcastReceiver { + final Service mService; + + InnerBroadcastReceiver(Service service) { + mService = service; + } + + @Override + public void onReceive(Context context, Intent intent) { + pollNetworkState(); + if (mStateChanged + && !isServiceRunning()) { + Log.d(Constants.TAG, "InnerBroadcastReceiver Called"); + Intent fileIntent = new Intent(context, mService.getClass()); + fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); + // send a new intent to the service + context.startService(fileIntent); + } + } + }; + + /** + * This is the main thread for the Downloader. This thread is responsible + * for queuing up downloads and other goodness. + */ + @Override + protected void onHandleIntent(Intent intent) { + setServiceRunning(true); + try { + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(this); + final PendingIntent pendingIntent = (PendingIntent) intent + .getParcelableExtra(EXTRA_PENDING_INTENT); + + if (null != pendingIntent) + { + mNotification.setClientIntent(pendingIntent); + mPendingIntent = pendingIntent; + } else if (null != mPendingIntent) { + mNotification.setClientIntent(mPendingIntent); + } else { + Log.e(LOG_TAG, "Downloader started in bad state without notification intent."); + return; + } + + // when the LVL check completes, a successful response will update + // the service + if (isLVLCheckRequired(db, mPackageInfo)) { + updateLVL(this); + return; + } + + // get each download + DownloadInfo[] infos = db.getDownloads(); + mBytesSoFar = 0; + mTotalLength = 0; + mFileCount = infos.length; + for (DownloadInfo info : infos) { + // We do an (simple) integrity check on each file, just to make + // sure + if (info.mStatus == STATUS_SUCCESS) { + // verify that the file matches the state + if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) { + info.mStatus = 0; + info.mCurrentBytes = 0; + } + } + // get aggregate data + mTotalLength += info.mTotalBytes; + mBytesSoFar += info.mCurrentBytes; + } + + // loop through all downloads and fetch them + pollNetworkState(); + if (null == mConnReceiver) { + + /** + * We use this to track network state, such as when WiFi, + * Cellular, etc. is enabled when downloads are paused or in + * progress. + */ + mConnReceiver = new InnerBroadcastReceiver(this); + IntentFilter intentFilter = new IntentFilter( + ConnectivityManager.CONNECTIVITY_ACTION); + intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + registerReceiver(mConnReceiver, intentFilter); + } + + for (DownloadInfo info : infos) { + long startingCount = info.mCurrentBytes; + + if (info.mStatus != STATUS_SUCCESS) { + DownloadThread dt = new DownloadThread(info, this, mNotification); + cancelAlarms(); + scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); + dt.run(); + cancelAlarms(); + } + db.updateFromDb(info); + boolean setWakeWatchdog = false; + int notifyStatus; + switch (info.mStatus) { + case STATUS_FORBIDDEN: + // the URL is out of date + updateLVL(this); + return; + case STATUS_SUCCESS: + mBytesSoFar += info.mCurrentBytes - startingCount; + db.updateMetadata(mPackageInfo.versionCode, 0); + continue; + case STATUS_FILE_DELIVERED_INCORRECTLY: + // we may be on a network that is returning us a web + // page on redirect + notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE; + info.mCurrentBytes = 0; + db.updateDownload(info); + setWakeWatchdog = true; + break; + case STATUS_PAUSED_BY_APP: + notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST; + break; + case STATUS_WAITING_FOR_NETWORK: + case STATUS_WAITING_TO_RETRY: + notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE; + setWakeWatchdog = true; + break; + case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION: + case STATUS_QUEUED_FOR_WIFI: + // look for more detail here + if (null != mWifiManager) { + if (!mWifiManager.isWifiEnabled()) { + notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION; + setWakeWatchdog = true; + break; + } + } + notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION; + setWakeWatchdog = true; + break; + case STATUS_CANCELED: + notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED; + setWakeWatchdog = true; + break; + + case STATUS_INSUFFICIENT_SPACE_ERROR: + notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL; + setWakeWatchdog = true; + break; + + case STATUS_DEVICE_NOT_FOUND_ERROR: + notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE; + setWakeWatchdog = true; + break; + + default: + notifyStatus = IDownloaderClient.STATE_FAILED; + break; + } + if (setWakeWatchdog) { + scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); + } else { + cancelAlarms(); + } + // failure or pause state + mNotification.onDownloadStateChanged(notifyStatus); + return; + } + + // all downloads complete + mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); + } finally { + setServiceRunning(false); + } + } + + @Override + public void onDestroy() { + if (null != mConnReceiver) { + unregisterReceiver(mConnReceiver); + mConnReceiver = null; + } + mServiceStub.disconnect(this); + super.onDestroy(); + } + + public int getNetworkAvailabilityState(DownloadsDB db) { + if (mIsConnected) { + if (!mIsCellularConnection) + return NETWORK_OK; + int flags = db.mFlags; + if (mIsRoaming) + return NETWORK_CANNOT_USE_ROAMING; + if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { + return NETWORK_OK; + } else { + return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; + } + } + return NETWORK_NO_CONNECTION; + } + + @Override + public void onCreate() { + super.onCreate(); + try { + mPackageInfo = getPackageManager().getPackageInfo( + getPackageName(), 0); + ApplicationInfo ai = getApplicationInfo(); + CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai); + mNotification = new DownloadNotification(this, applicationLabel); + + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + } + + /** + * Exception thrown from methods called by generateSaveFile() for any fatal + * error. + */ + public static class GenerateSaveFileError extends Exception { + private static final long serialVersionUID = 3465966015408936540L; + int mStatus; + String mMessage; + + public GenerateSaveFileError(int status, String message) { + mStatus = status; + mMessage = message; + } + } + + /** + * Returns the filename (where the file should be saved) from info about a + * download + */ + public String generateTempSaveFileName(String fileName) { + String path = Helpers.getSaveFilePath(this) + + File.separator + fileName + TEMP_EXT; + return path; + } + + /** + * Creates a filename (where the file should be saved) from info about a + * download. + */ + public String generateSaveFile(String filename, long filesize) + throws GenerateSaveFileError { + String path = generateTempSaveFileName(filename); + File expPath = new File(path); + if (!Helpers.isExternalMediaMounted()) { + Log.d(Constants.TAG, "External media not mounted: " + path); + throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR, + "external media is not yet mounted"); + + } + if (expPath.exists()) { + Log.d(Constants.TAG, "File already exists: " + path); + throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR, + "requested destination file already exists"); + } + if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) { + throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR, + "insufficient space on external storage"); + } + return path; + } + + /** + * @return a non-localized string appropriate for logging corresponding to + * one of the NETWORK_* constants. + */ + public String getLogMessageForNetworkError(int networkError) { + switch (networkError) { + case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: + return "download size exceeds recommended limit for mobile network"; + + case NETWORK_UNUSABLE_DUE_TO_SIZE: + return "download size exceeds limit for mobile network"; + + case NETWORK_NO_CONNECTION: + return "no network connection available"; + + case NETWORK_CANNOT_USE_ROAMING: + return "download cannot use the current network connection because it is roaming"; + + case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: + return "download was requested to not use the current network type"; + + default: + return "unknown error with network connectivity"; + } + } + + public int getControl() { + return mControl; + } + + public int getStatus() { + return mStatus; + } + + /** + * Calculating a moving average for the speed so we don't get jumpy + * calculations for time etc. + */ + static private final float SMOOTHING_FACTOR = 0.005f; + + public void notifyUpdateBytes(long totalBytesSoFar) { + long timeRemaining; + long currentTime = SystemClock.uptimeMillis(); + if (0 != mMillisecondsAtSample) { + // we have a sample. + long timePassed = currentTime - mMillisecondsAtSample; + long bytesInSample = totalBytesSoFar - mBytesAtSample; + float currentSpeedSample = (float) bytesInSample / (float) timePassed; + if (0 != mAverageDownloadSpeed) { + mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample + + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed; + } else { + mAverageDownloadSpeed = currentSpeedSample; + } + timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed); + } else { + timeRemaining = -1; + } + mMillisecondsAtSample = currentTime; + mBytesAtSample = totalBytesSoFar; + mNotification.onDownloadProgress( + new DownloadProgressInfo(mTotalLength, + totalBytesSoFar, + timeRemaining, + mAverageDownloadSpeed) + ); + + } + + @Override + protected boolean shouldStop() { + // the database automatically reads the metadata for version code + // and download status when the instance is created + DownloadsDB db = DownloadsDB.getDB(this); + if (db.mStatus == 0) { + return true; + } + return false; + } + + @Override + public void requestDownloadStatus() { + mNotification.resendState(); + } + + @Override + public void onClientUpdated(Messenger clientMessenger) { + this.mClientMessenger = clientMessenger; + mNotification.setMessenger(mClientMessenger); + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java new file mode 100644 index 0000000000..c658b4cc43 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java @@ -0,0 +1,510 @@ +/* + * 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 android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.provider.BaseColumns; +import android.util.Log; + +public class DownloadsDB { + private static final String DATABASE_NAME = "DownloadsDB"; + private static final int DATABASE_VERSION = 7; + public static final String LOG_TAG = DownloadsDB.class.getName(); + final SQLiteOpenHelper mHelper; + SQLiteStatement mGetDownloadByIndex; + SQLiteStatement mUpdateCurrentBytes; + private static DownloadsDB mDownloadsDB; + long mMetadataRowID = -1; + int mVersionCode = -1; + int mStatus = -1; + int mFlags; + + static public synchronized DownloadsDB getDB(Context paramContext) { + if (null == mDownloadsDB) { + return new DownloadsDB(paramContext); + } + return mDownloadsDB; + } + + private SQLiteStatement getDownloadByIndexStatement() { + if (null == mGetDownloadByIndex) { + mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( + "SELECT " + BaseColumns._ID + " FROM " + + DownloadColumns.TABLE_NAME + " WHERE " + + DownloadColumns.INDEX + " = ?"); + } + return mGetDownloadByIndex; + } + + private SQLiteStatement getUpdateCurrentBytesStatement() { + if (null == mUpdateCurrentBytes) { + mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( + "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + + " = ?" + + " WHERE " + DownloadColumns.INDEX + " = ?"); + } + return mUpdateCurrentBytes; + } + + private DownloadsDB(Context paramContext) { + this.mHelper = new DownloadsContentDBHelper(paramContext); + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + // Query for the version code, the row ID of the metadata (for future + // updating) the status and the flags + Cursor cur = sqldb.rawQuery("SELECT " + + MetadataColumns.APKVERSION + "," + + BaseColumns._ID + "," + + MetadataColumns.DOWNLOAD_STATUS + "," + + MetadataColumns.FLAGS + + " FROM " + + MetadataColumns.TABLE_NAME + " LIMIT 1", null); + if (null != cur && cur.moveToFirst()) { + mVersionCode = cur.getInt(0); + mMetadataRowID = cur.getLong(1); + mStatus = cur.getInt(2); + mFlags = cur.getInt(3); + cur.close(); + } + mDownloadsDB = this; + } + + protected DownloadInfo getDownloadInfoByFileName(String fileName) { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor itemcur = null; + try { + itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, + DownloadColumns.FILENAME + " = ?", + new String[] { + fileName + }, null, null, null); + if (null != itemcur && itemcur.moveToFirst()) { + return getDownloadInfoFromCursor(itemcur); + } + } finally { + if (null != itemcur) + itemcur.close(); + } + return null; + } + + public long getIDForDownloadInfo(final DownloadInfo di) { + return getIDByIndex(di.mIndex); + } + + public long getIDByIndex(int index) { + SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); + downloadByIndex.clearBindings(); + downloadByIndex.bindLong(1, index); + try { + return downloadByIndex.simpleQueryForLong(); + } catch (SQLiteDoneException e) { + return -1; + } + } + + public void updateDownloadCurrentBytes(final DownloadInfo di) { + SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); + downloadCurrentBytes.clearBindings(); + downloadCurrentBytes.bindLong(1, di.mCurrentBytes); + downloadCurrentBytes.bindLong(2, di.mIndex); + downloadCurrentBytes.execute(); + } + + public void close() { + this.mHelper.close(); + } + + protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { + DownloadsContentDBHelper(Context paramContext) { + super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); + } + + private String createTableQueryFromArray(String paramString, + String[][] paramArrayOfString) { + StringBuilder localStringBuilder = new StringBuilder(); + localStringBuilder.append("CREATE TABLE "); + localStringBuilder.append(paramString); + localStringBuilder.append(" ("); + int i = paramArrayOfString.length; + for (int j = 0;; j++) { + if (j >= i) { + localStringBuilder + .setLength(localStringBuilder.length() - 1); + localStringBuilder.append(");"); + return localStringBuilder.toString(); + } + String[] arrayOfString = paramArrayOfString[j]; + localStringBuilder.append(' '); + localStringBuilder.append(arrayOfString[0]); + localStringBuilder.append(' '); + localStringBuilder.append(arrayOfString[1]); + localStringBuilder.append(','); + } + } + + /** + * These two arrays must match and have the same order. For every Schema + * there must be a corresponding table name. + */ + static final private String[][][] sSchemas = { + DownloadColumns.SCHEMA, MetadataColumns.SCHEMA + }; + + static final private String[] sTables = { + DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME + }; + + /** + * Goes through all of the tables in sTables and drops each table if it + * exists. Altered to no longer make use of reflection. + */ + private void dropTables(SQLiteDatabase paramSQLiteDatabase) { + for (String table : sTables) { + try { + paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); + } catch (Exception localException) { + localException.printStackTrace(); + } + } + } + + /** + * Goes through all of the tables in sTables and creates a database with + * the corresponding schema described in sSchemas. Altered to no longer + * make use of reflection. + */ + public void onCreate(SQLiteDatabase paramSQLiteDatabase) { + int numSchemas = sSchemas.length; + for (int i = 0; i < numSchemas; i++) { + try { + String[][] schema = (String[][]) sSchemas[i]; + paramSQLiteDatabase.execSQL(createTableQueryFromArray( + sTables[i], schema)); + } catch (Exception localException) { + while (true) + localException.printStackTrace(); + } + } + } + + public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, + int paramInt1, int paramInt2) { + Log.w(DownloadsContentDBHelper.class.getName(), + "Upgrading database from version " + paramInt1 + " to " + + paramInt2 + ", which will destroy all old data"); + dropTables(paramSQLiteDatabase); + onCreate(paramSQLiteDatabase); + } + } + + public static class MetadataColumns implements BaseColumns { + public static final String APKVERSION = "APKVERSION"; + public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; + public static final String FLAGS = "DOWNLOADFLAGS"; + + public static final String[][] SCHEMA = { + { + BaseColumns._ID, "INTEGER PRIMARY KEY" + }, + { + APKVERSION, "INTEGER" + }, { + DOWNLOAD_STATUS, "INTEGER" + }, + { + FLAGS, "INTEGER" + } + }; + public static final String TABLE_NAME = "MetadataColumns"; + public static final String _ID = "MetadataColumns._id"; + } + + public static class DownloadColumns implements BaseColumns { + public static final String INDEX = "FILEIDX"; + public static final String URI = "URI"; + public static final String FILENAME = "FN"; + public static final String ETAG = "ETAG"; + + public static final String TOTALBYTES = "TOTALBYTES"; + public static final String CURRENTBYTES = "CURRENTBYTES"; + public static final String LASTMOD = "LASTMOD"; + + public static final String STATUS = "STATUS"; + public static final String CONTROL = "CONTROL"; + public static final String NUM_FAILED = "FAILCOUNT"; + public static final String RETRY_AFTER = "RETRYAFTER"; + public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; + + public static final String[][] SCHEMA = { + { + BaseColumns._ID, "INTEGER PRIMARY KEY" + }, + { + INDEX, "INTEGER UNIQUE" + }, { + URI, "TEXT" + }, + { + FILENAME, "TEXT UNIQUE" + }, { + ETAG, "TEXT" + }, + { + TOTALBYTES, "INTEGER" + }, { + CURRENTBYTES, "INTEGER" + }, + { + LASTMOD, "INTEGER" + }, { + STATUS, "INTEGER" + }, + { + CONTROL, "INTEGER" + }, { + NUM_FAILED, "INTEGER" + }, + { + RETRY_AFTER, "INTEGER" + }, { + REDIRECT_COUNT, "INTEGER" + } + }; + public static final String TABLE_NAME = "DownloadColumns"; + public static final String _ID = "DownloadColumns._id"; + } + + private static final String[] DC_PROJECTION = { + DownloadColumns.FILENAME, + DownloadColumns.URI, DownloadColumns.ETAG, + DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, + DownloadColumns.LASTMOD, DownloadColumns.STATUS, + DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, + DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, + DownloadColumns.INDEX + }; + + private static final int FILENAME_IDX = 0; + private static final int URI_IDX = 1; + private static final int ETAG_IDX = 2; + private static final int TOTALBYTES_IDX = 3; + private static final int CURRENTBYTES_IDX = 4; + private static final int LASTMOD_IDX = 5; + private static final int STATUS_IDX = 6; + private static final int CONTROL_IDX = 7; + private static final int NUM_FAILED_IDX = 8; + private static final int RETRY_AFTER_IDX = 9; + private static final int REDIRECT_COUNT_IDX = 10; + private static final int INDEX_IDX = 11; + + /** + * This function will add a new file to the database if it does not exist. + * + * @param di DownloadInfo that we wish to store + * @return the row id of the record to be updated/inserted, or -1 + */ + public boolean updateDownload(DownloadInfo di) { + ContentValues cv = new ContentValues(); + cv.put(DownloadColumns.INDEX, di.mIndex); + cv.put(DownloadColumns.FILENAME, di.mFileName); + cv.put(DownloadColumns.URI, di.mUri); + cv.put(DownloadColumns.ETAG, di.mETag); + cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); + cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); + cv.put(DownloadColumns.LASTMOD, di.mLastMod); + cv.put(DownloadColumns.STATUS, di.mStatus); + cv.put(DownloadColumns.CONTROL, di.mControl); + cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); + cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); + cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); + return updateDownload(di, cv); + } + + public boolean updateDownload(DownloadInfo di, ContentValues cv) { + long id = di == null ? -1 : getIDForDownloadInfo(di); + try { + final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); + if (id != -1) { + if (1 != sqldb.update(DownloadColumns.TABLE_NAME, + cv, DownloadColumns._ID + " = " + id, null)) { + return false; + } + } else { + return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, + DownloadColumns.URI, cv); + } + } catch (android.database.sqlite.SQLiteException ex) { + ex.printStackTrace(); + } + return false; + } + + public int getLastCheckedVersionCode() { + return mVersionCode; + } + + public boolean isDownloadRequired() { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + + DownloadColumns.TABLE_NAME + " WHERE " + + DownloadColumns.STATUS + " <> 0", null); + try { + if (null != cur && cur.moveToFirst()) { + return 0 == cur.getInt(0); + } + } finally { + if (null != cur) + cur.close(); + } + return true; + } + + public int getFlags() { + return mFlags; + } + + public boolean updateFlags(int flags) { + if (mFlags != flags) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.FLAGS, flags); + if (updateMetadata(cv)) { + mFlags = flags; + return true; + } else { + return false; + } + } else { + return true; + } + }; + + public boolean updateStatus(int status) { + if (mStatus != status) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.DOWNLOAD_STATUS, status); + if (updateMetadata(cv)) { + mStatus = status; + return true; + } else { + return false; + } + } else { + return true; + } + }; + + public boolean updateMetadata(ContentValues cv) { + final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); + if (-1 == this.mMetadataRowID) { + long newID = sqldb.insert(MetadataColumns.TABLE_NAME, + MetadataColumns.APKVERSION, cv); + if (-1 == newID) + return false; + mMetadataRowID = newID; + } else { + if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, + BaseColumns._ID + " = " + mMetadataRowID, null)) + return false; + } + return true; + } + + public boolean updateMetadata(int apkVersion, int downloadStatus) { + ContentValues cv = new ContentValues(); + cv.put(MetadataColumns.APKVERSION, apkVersion); + cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); + if (updateMetadata(cv)) { + mVersionCode = apkVersion; + mStatus = downloadStatus; + return true; + } else { + return false; + } + }; + + public boolean updateFromDb(DownloadInfo di) { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = null; + try { + cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, + DownloadColumns.FILENAME + "= ?", + new String[] { + di.mFileName + }, null, null, null); + if (null != cur && cur.moveToFirst()) { + setDownloadInfoFromCursor(di, cur); + return true; + } + return false; + } finally { + if (null != cur) { + cur.close(); + } + } + } + + public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { + di.mUri = cur.getString(URI_IDX); + di.mETag = cur.getString(ETAG_IDX); + di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); + di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); + di.mLastMod = cur.getLong(LASTMOD_IDX); + di.mStatus = cur.getInt(STATUS_IDX); + di.mControl = cur.getInt(CONTROL_IDX); + di.mNumFailed = cur.getInt(NUM_FAILED_IDX); + di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); + di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); + } + + public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { + DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), + cur.getString(FILENAME_IDX), this.getClass().getPackage() + .getName()); + setDownloadInfoFromCursor(di, cur); + return di; + } + + public DownloadInfo[] getDownloads() { + final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); + Cursor cur = null; + try { + cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, + null, null, null, null); + if (null != cur && cur.moveToFirst()) { + DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; + int idx = 0; + do { + DownloadInfo di = getDownloadInfoFromCursor(cur); + retInfos[idx++] = di; + } while (cur.moveToNext()); + return retInfos; + } + return null; + } finally { + if (null != cur) { + cur.close(); + } + } + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java new file mode 100644 index 0000000000..3f440e9893 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java @@ -0,0 +1,200 @@ +/* + * 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 android.text.format.Time; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper for parsing an HTTP date. + */ +public final class HttpDateTime { + + /* + * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT + * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850, + * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format + * with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon + * YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS + * GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon + * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first + * digit is zero. Mon can be the full name of the month. + */ + private static final String HTTP_DATE_RFC_REGEXP = + "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; + + private static final String HTTP_DATE_ANSIC_REGEXP = + "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + /** + * The compiled version of the HTTP-date regular expressions. + */ + private static final Pattern HTTP_DATE_RFC_PATTERN = + Pattern.compile(HTTP_DATE_RFC_REGEXP); + private static final Pattern HTTP_DATE_ANSIC_PATTERN = + Pattern.compile(HTTP_DATE_ANSIC_REGEXP); + + private static class TimeOfDay { + TimeOfDay(int h, int m, int s) { + this.hour = h; + this.minute = m; + this.second = s; + } + + int hour; + int minute; + int second; + } + + public static long parse(String timeString) + throws IllegalArgumentException { + + int date = 1; + int month = Calendar.JANUARY; + int year = 1970; + TimeOfDay timeOfDay; + + Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); + if (rfcMatcher.find()) { + date = getDate(rfcMatcher.group(1)); + month = getMonth(rfcMatcher.group(2)); + year = getYear(rfcMatcher.group(3)); + timeOfDay = getTime(rfcMatcher.group(4)); + } else { + Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); + if (ansicMatcher.find()) { + month = getMonth(ansicMatcher.group(1)); + date = getDate(ansicMatcher.group(2)); + timeOfDay = getTime(ansicMatcher.group(3)); + year = getYear(ansicMatcher.group(4)); + } else { + throw new IllegalArgumentException(); + } + } + + // FIXME: Y2038 BUG! + if (year >= 2038) { + year = 2038; + month = Calendar.JANUARY; + date = 1; + } + + Time time = new Time(Time.TIMEZONE_UTC); + time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, + month, year); + return time.toMillis(false /* use isDst */); + } + + private static int getDate(String dateString) { + if (dateString.length() == 2) { + return (dateString.charAt(0) - '0') * 10 + + (dateString.charAt(1) - '0'); + } else { + return (dateString.charAt(0) - '0'); + } + } + + /* + * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0 + * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20 + * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19 + * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9 + */ + private static int getMonth(String monthString) { + int hash = Character.toLowerCase(monthString.charAt(0)) + + Character.toLowerCase(monthString.charAt(1)) + + Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; + switch (hash) { + case 22: + return Calendar.JANUARY; + case 10: + return Calendar.FEBRUARY; + case 29: + return Calendar.MARCH; + case 32: + return Calendar.APRIL; + case 36: + return Calendar.MAY; + case 42: + return Calendar.JUNE; + case 40: + return Calendar.JULY; + case 26: + return Calendar.AUGUST; + case 37: + return Calendar.SEPTEMBER; + case 35: + return Calendar.OCTOBER; + case 48: + return Calendar.NOVEMBER; + case 9: + return Calendar.DECEMBER; + default: + throw new IllegalArgumentException(); + } + } + + private static int getYear(String yearString) { + if (yearString.length() == 2) { + int year = (yearString.charAt(0) - '0') * 10 + + (yearString.charAt(1) - '0'); + if (year >= 70) { + return year + 1900; + } else { + return year + 2000; + } + } else if (yearString.length() == 3) { + // According to RFC 2822, three digit years should be added to 1900. + int year = (yearString.charAt(0) - '0') * 100 + + (yearString.charAt(1) - '0') * 10 + + (yearString.charAt(2) - '0'); + return year + 1900; + } else if (yearString.length() == 4) { + return (yearString.charAt(0) - '0') * 1000 + + (yearString.charAt(1) - '0') * 100 + + (yearString.charAt(2) - '0') * 10 + + (yearString.charAt(3) - '0'); + } else { + return 1970; + } + } + + private static TimeOfDay getTime(String timeString) { + // HH might be H + int i = 0; + int hour = timeString.charAt(i++) - '0'; + if (timeString.charAt(i) != ':') + hour = hour * 10 + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int minute = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int second = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + + return new TimeOfDay(hour, minute, second); + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java new file mode 100644 index 0000000000..d6ccb0c5e4 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * An Obfuscator that uses AES to encrypt data. + */ +public class AESObfuscator implements Obfuscator { + private static final String UTF8 = "UTF-8"; + private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final byte[] IV = + { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; + private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; + + private Cipher mEncryptor; + private Cipher mDecryptor; + + /** + * @param salt an array of random bytes to use for each (un)obfuscation + * @param applicationId application identifier, e.g. the package name + * @param deviceId device identifier. Use as many sources as possible to + * create this unique identifier. + */ + public AESObfuscator(byte[] salt, String applicationId, String deviceId) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); + KeySpec keySpec = + new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); + SecretKey tmp = factory.generateSecret(keySpec); + SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); + mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); + mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); + } catch (GeneralSecurityException e) { + // This can't happen on a compatible Android device. + throw new RuntimeException("Invalid environment", e); + } + } + + public String obfuscate(String original, String key) { + if (original == null) { + return null; + } + try { + // Header is appended as an integrity check + return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid environment", e); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Invalid environment", e); + } + } + + public String unobfuscate(String obfuscated, String key) throws ValidationException { + if (obfuscated == null) { + return null; + } + try { + String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); + // Check for presence of header. This serves as a final integrity check, for cases + // where the block size is correct during decryption. + int headerIndex = result.indexOf(header+key); + if (headerIndex != 0) { + throw new ValidationException("Header not found (invalid data or key)" + ":" + + obfuscated); + } + return result.substring(header.length()+key.length(), result.length()); + } catch (Base64DecoderException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (IllegalBlockSizeException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (BadPaddingException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid environment", e); + } + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000000..37fad8926a --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -0,0 +1,414 @@ + +package com.google.android.vending.licensing; + +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.vending.licensing.util.URIQueryDecoder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +/** + * Default policy. All policy decisions are based off of response data received + * from the licensing service. Specifically, the licensing server sends the + * following information: response validity period, error retry period, + * error retry count and a URL for restoring app access in unlicensed cases. + * <p> + * These values will vary based on the the way the application is configured in + * the Google Play publishing console, such as whether the application is + * marked as free or is within its refund period, as well as how often an + * application is checking with the licensing service. + * <p> + * Developers who need more fine grained control over their application's + * licensing policy should implement a custom Policy. + */ +public class APKExpansionPolicy implements Policy { + + private static final String TAG = "APKExpansionPolicy"; + private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy"; + private static final String PREF_LAST_RESPONSE = "lastResponse"; + private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; + private static final String PREF_RETRY_UNTIL = "retryUntil"; + private static final String PREF_MAX_RETRIES = "maxRetries"; + private static final String PREF_RETRY_COUNT = "retryCount"; + private static final String PREF_LICENSING_URL = "licensingUrl"; + private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; + private static final String DEFAULT_RETRY_UNTIL = "0"; + private static final String DEFAULT_MAX_RETRIES = "0"; + private static final String DEFAULT_RETRY_COUNT = "0"; + + private static final long MILLIS_PER_MINUTE = 60 * 1000; + + private long mValidityTimestamp; + private long mRetryUntil; + private long mMaxRetries; + private long mRetryCount; + private long mLastResponseTime = 0; + private int mLastResponse; + private String mLicensingUrl; + private PreferenceObfuscator mPreferences; + private Vector<String> mExpansionURLs = new Vector<String>(); + private Vector<String> mExpansionFileNames = new Vector<String>(); + private Vector<Long> mExpansionFileSizes = new Vector<Long>(); + + /** + * The design of the protocol supports n files. Currently the market can + * only deliver two files. To accommodate this, we have these two constants, + * but the order is the only relevant thing here. + */ + public static final int MAIN_FILE_URL_INDEX = 0; + public static final int PATCH_FILE_URL_INDEX = 1; + + /** + * @param context The context for the current application + * @param obfuscator An obfuscator to be used with preferences. + */ + public APKExpansionPolicy(Context context, Obfuscator obfuscator) { + // Import old values + SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); + mPreferences = new PreferenceObfuscator(sp, obfuscator); + mLastResponse = Integer.parseInt( + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); + mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, + DEFAULT_VALIDITY_TIMESTAMP)); + mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); + mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); + mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); + mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); + } + + /** + * We call this to guarantee that we fetch a fresh policy from the server. + * This is to be used if the URL is invalid. + */ + public void resetPolicy() { + mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + mPreferences.commit(); + } + + /** + * Process a new response from the license server. + * <p> + * This data will be used for computing future policy decisions. The + * following parameters are processed: + * <ul> + * <li>VT: the timestamp that the client should consider the response valid + * until + * <li>GT: the timestamp that the client should ignore retry errors until + * <li>GR: the number of retry errors that the client should ignore + * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. + * buy app on the Play Store) + * </ul> + * + * @param response the result from validating the server response + * @param rawData the raw server response data + */ + public void processServerResponse(int response, + com.google.android.vending.licensing.ResponseData rawData) { + + // Update retry counter + if (response != Policy.RETRY) { + setRetryCount(0); + } else { + setRetryCount(mRetryCount + 1); + } + + // Update server policy data + Map<String, String> extras = decodeExtras(rawData); + if (response == Policy.LICENSED) { + mLastResponse = response; + // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. + setLicensingUrl(null); + setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); + Set<String> keys = extras.keySet(); + for (String key : keys) { + if (key.equals("VT")) { + setValidityTimestamp(extras.get(key)); + } else if (key.equals("GT")) { + setRetryUntil(extras.get(key)); + } else if (key.equals("GR")) { + setMaxRetries(extras.get(key)); + } else if (key.startsWith("FILE_URL")) { + int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; + setExpansionURL(index, extras.get(key)); + } else if (key.startsWith("FILE_NAME")) { + int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; + setExpansionFileName(index, extras.get(key)); + } else if (key.startsWith("FILE_SIZE")) { + int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; + setExpansionFileSize(index, Long.parseLong(extras.get(key))); + } + } + } else if (response == Policy.NOT_LICENSED) { + // Clear out stale retry params + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + // Update the licensing URL + setLicensingUrl(extras.get("LU")); + } + + setLastResponse(response); + mPreferences.commit(); + } + + /** + * Set the last license response received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param l the response + */ + private void setLastResponse(int l) { + mLastResponseTime = System.currentTimeMillis(); + mLastResponse = l; + mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); + } + + /** + * Set the current retry count and add to preferences. You must manually + * call PreferenceObfuscator.commit() to commit these changes to disk. + * + * @param c the new retry count + */ + private void setRetryCount(long c) { + mRetryCount = c; + mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); + } + + public long getRetryCount() { + return mRetryCount; + } + + /** + * Set the last validity timestamp (VT) received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param validityTimestamp the VT string received + */ + private void setValidityTimestamp(String validityTimestamp) { + Long lValidityTimestamp; + try { + lValidityTimestamp = Long.parseLong(validityTimestamp); + } catch (NumberFormatException e) { + // No response or not parseable, expire in one minute. + Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); + lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; + validityTimestamp = Long.toString(lValidityTimestamp); + } + + mValidityTimestamp = lValidityTimestamp; + mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); + } + + public long getValidityTimestamp() { + return mValidityTimestamp; + } + + /** + * Set the retry until timestamp (GT) received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param retryUntil the GT string received + */ + private void setRetryUntil(String retryUntil) { + Long lRetryUntil; + try { + lRetryUntil = Long.parseLong(retryUntil); + } catch (NumberFormatException e) { + // No response or not parseable, expire immediately + Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); + retryUntil = "0"; + lRetryUntil = 0l; + } + + mRetryUntil = lRetryUntil; + mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); + } + + public long getRetryUntil() { + return mRetryUntil; + } + + /** + * Set the max retries value (GR) as received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param maxRetries the GR string received + */ + private void setMaxRetries(String maxRetries) { + Long lMaxRetries; + try { + lMaxRetries = Long.parseLong(maxRetries); + } catch (NumberFormatException e) { + // No response or not parseable, expire immediately + Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); + maxRetries = "0"; + lMaxRetries = 0l; + } + + mMaxRetries = lMaxRetries; + mPreferences.putString(PREF_MAX_RETRIES, maxRetries); + } + + public long getMaxRetries() { + return mMaxRetries; + } + + /** + * Set the licensing URL that displays a Play Store UI for the user to regain app access. + * + * @param url the LU string received + */ + private void setLicensingUrl(String url) { + mLicensingUrl = url; + mPreferences.putString(PREF_LICENSING_URL, url); + } + + public String getLicensingUrl() { + return mLicensingUrl; + } + + /** + * Gets the count of expansion URLs. Since expansionURLs are not committed + * to preferences, this will return zero if there has been no LVL fetch + * in the current session. + * + * @return the number of expansion URLs. (0,1,2) + */ + public int getExpansionURLCount() { + return mExpansionURLs.size(); + } + + /** + * Gets the expansion URL. Since these URLs are not committed to + * preferences, this will always return null if there has not been an LVL + * fetch in the current session. + * + * @param index the index of the URL to fetch. This value will be either + * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX + */ + public String getExpansionURL(int index) { + if (index < mExpansionURLs.size()) { + return mExpansionURLs.elementAt(index); + } + return null; + } + + /** + * Sets the expansion URL. Expansion URL's are not committed to preferences, + * but are instead intended to be stored when the license response is + * processed by the front-end. + * + * @param index the index of the expansion URL. This value will be either + * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX + * @param URL the URL to set + */ + public void setExpansionURL(int index, String URL) { + if (index >= mExpansionURLs.size()) { + mExpansionURLs.setSize(index + 1); + } + mExpansionURLs.set(index, URL); + } + + public String getExpansionFileName(int index) { + if (index < mExpansionFileNames.size()) { + return mExpansionFileNames.elementAt(index); + } + return null; + } + + public void setExpansionFileName(int index, String name) { + if (index >= mExpansionFileNames.size()) { + mExpansionFileNames.setSize(index + 1); + } + mExpansionFileNames.set(index, name); + } + + public long getExpansionFileSize(int index) { + if (index < mExpansionFileSizes.size()) { + return mExpansionFileSizes.elementAt(index); + } + return -1; + } + + public void setExpansionFileSize(int index, long size) { + if (index >= mExpansionFileSizes.size()) { + mExpansionFileSizes.setSize(index + 1); + } + mExpansionFileSizes.set(index, size); + } + + /** + * {@inheritDoc} This implementation allows access if either:<br> + * <ol> + * <li>a LICENSED response was received within the validity period + * <li>a RETRY response was received in the last minute, and we are under + * the RETRY count or in the RETRY period. + * </ol> + */ + public boolean allowAccess() { + long ts = System.currentTimeMillis(); + if (mLastResponse == Policy.LICENSED) { + // Check if the LICENSED response occurred within the validity + // timeout. + if (ts <= mValidityTimestamp) { + // Cached LICENSED response is still valid. + return true; + } + } else if (mLastResponse == Policy.RETRY && + ts < mLastResponseTime + MILLIS_PER_MINUTE) { + // Only allow access if we are within the retry period or we haven't + // used up our + // max retries. + return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); + } + return false; + } + + private Map<String, String> decodeExtras( + com.google.android.vending.licensing.ResponseData rawData) { + Map<String, String> results = new HashMap<String, String>(); + if (rawData == null) { + return results; + } + + try { + URI rawExtras = new URI("?" + rawData.extra); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } + +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java index 2384b8b82f..e5c5e2d7ca 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/DeviceLimiter.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java @@ -37,11 +37,11 @@ package com.google.android.vending.licensing; */ public interface DeviceLimiter { - /** + /** * Checks if this device is allowed to use the given user's license. * * @param userId the user whose license the server responded with * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs */ - int isDeviceAllowed(String userId); + int isDeviceAllowed(String userId); } diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..15017b3425 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,389 @@ +/* + * 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.android.vending.licensing.ILicenseResultListener; +import com.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.getString(mContext.getContentResolver(), + Secure.ANDROID_ID); + Date date = new Date(); + Log.d(TAG, "Server Failure: " + stringError); + Log.d(TAG, "Android ID: " + android_id); + Log.d(TAG, "Time: " + date.toGMTString()); + } + } + + } + }); + } + + private void startTimeout() { + Log.i(TAG, "Start monitoring timeout."); + mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); + } + + private void clearTimeout() { + Log.i(TAG, "Clearing timeout."); + mHandler.removeCallbacks(mOnTimeout); + } + } + + public synchronized void onServiceConnected(ComponentName name, IBinder service) { + mService = ILicensingService.Stub.asInterface(service); + runChecks(); + } + + public synchronized void onServiceDisconnected(ComponentName name) { + // Called when the connection with the service has been + // unexpectedly disconnected. That is, Market crashed. + // If there are any checks in progress, the timeouts will handle them. + Log.w(TAG, "Service unexpectedly disconnected."); + mService = null; + } + + /** + * Generates policy response for service connection errors, as a result of disconnections or + * timeouts. + */ + private synchronized void handleServiceConnectionError(LicenseValidator validator) { + mPolicy.processServerResponse(Policy.RETRY, null); + + if (mPolicy.allowAccess()) { + validator.getCallback().allow(Policy.RETRY); + } else { + validator.getCallback().dontAllow(Policy.RETRY); + } + } + + /** Unbinds service if necessary and removes reference to it. */ + private void cleanupService() { + if (mService != null) { + try { + mContext.unbindService(this); + } catch (IllegalArgumentException e) { + // Somehow we've already been unbound. This is a non-fatal + // error. + Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); + } + mService = null; + } + } + + /** + * Inform the library that the context is about to be destroyed, so that any open connections + * can be cleaned up. + * <p> + * Failure to call this method can result in a crash under certain circumstances, such as during + * screen rotation if an Activity requests the license check or when the user exits the + * application. + */ + public synchronized void onDestroy() { + cleanupService(); + mHandler.getLooper().quit(); + } + + /** Generates a nonce (number used once). */ + private int generateNonce() { + return RANDOM.nextInt(); + } + + /** + * Get version code for the application package name. + * + * @param context + * @param packageName application package name + * @return the version code or empty string if package not found + */ + private static String getVersionCode(Context context, String packageName) { + try { + return String.valueOf( + context.getPackageManager().getPackageInfo(packageName, 0).versionCode); + } catch (NameNotFoundException e) { + Log.e(TAG, "Package not found. could not get version code."); + return ""; + } + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java index dc2c2d70bf..8b869ddaaf 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/LicenseCheckerCallback.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -34,34 +34,34 @@ package com.google.android.vending.licensing; */ public interface LicenseCheckerCallback { - /** + /** * Allow use. App should proceed as normal. * * @param reason Policy.LICENSED or Policy.RETRY typically. (although in * theory the policy can return Policy.NOT_LICENSED here as well) */ - public void allow(int reason); + public void allow(int reason); - /** + /** * Don't allow use. App should inform user and take appropriate action. * * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory * the policy can return Policy.LICENSED here as well --- * perhaps the call to the LVL took too long, for example) */ - public void dontAllow(int reason); + public void dontAllow(int reason); - /** Application error codes. */ - public static final int ERROR_INVALID_PACKAGE_NAME = 1; - public static final int ERROR_NON_MATCHING_UID = 2; - public static final int ERROR_NOT_MARKET_MANAGED = 3; - public static final int ERROR_CHECK_IN_PROGRESS = 4; - public static final int ERROR_INVALID_PUBLIC_KEY = 5; - public static final int ERROR_MISSING_PERMISSION = 6; + /** Application error codes. */ + public static final int ERROR_INVALID_PACKAGE_NAME = 1; + public static final int ERROR_NON_MATCHING_UID = 2; + public static final int ERROR_NOT_MARKET_MANAGED = 3; + public static final int ERROR_CHECK_IN_PROGRESS = 4; + public static final int ERROR_INVALID_PUBLIC_KEY = 5; + public static final int ERROR_MISSING_PERMISSION = 6; - /** + /** * Error in application code. Caller did not call or set up license checker * correctly. Should be considered fatal. */ - public void applicationError(int errorCode); + public void applicationError(int errorCode); } diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java new file mode 100644 index 0000000000..11a00786d0 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import android.text.TextUtils; +import android.util.Log; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * Contains data related to a licensing request and methods to verify + * and process the response. + */ +class LicenseValidator { + private static final String TAG = "LicenseValidator"; + + // Server response codes. + private static final int LICENSED = 0x0; + private static final int NOT_LICENSED = 0x1; + private static final int LICENSED_OLD_KEY = 0x2; + private static final int ERROR_NOT_MARKET_MANAGED = 0x3; + private static final int ERROR_SERVER_FAILURE = 0x4; + private static final int ERROR_OVER_QUOTA = 0x5; + + private static final int ERROR_CONTACTING_SERVER = 0x101; + private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + private static final int ERROR_NON_MATCHING_UID = 0x103; + + private final Policy mPolicy; + private final LicenseCheckerCallback mCallback; + private final int mNonce; + private final String mPackageName; + private final String mVersionCode; + private final DeviceLimiter mDeviceLimiter; + + LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, + int nonce, String packageName, String versionCode) { + mPolicy = policy; + mDeviceLimiter = deviceLimiter; + mCallback = callback; + mNonce = nonce; + mPackageName = packageName; + mVersionCode = versionCode; + } + + public LicenseCheckerCallback getCallback() { + return mCallback; + } + + public int getNonce() { + return mNonce; + } + + public String getPackageName() { + return mPackageName; + } + + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + /** + * Verifies the response from server and calls appropriate callback method. + * + * @param publicKey public key associated with the developer account + * @param responseCode server response code + * @param signedData signed data from server + * @param signature server signature + */ + public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { + String userId = null; + // Skip signature check for unsuccessful requests + ResponseData data = null; + if (responseCode == LICENSED || responseCode == NOT_LICENSED || + responseCode == LICENSED_OLD_KEY) { + // Verify signature. + try { + if (TextUtils.isEmpty(signedData)) { + Log.e(TAG, "Signature verification failed: signedData is empty. " + + "(Device not signed-in to any Google accounts?)"); + handleInvalidResponse(); + return; + } + + Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + handleInvalidResponse(); + return; + } + } catch (NoSuchAlgorithmException e) { + // This can't happen on an Android compatible device. + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); + return; + } catch (SignatureException e) { + throw new RuntimeException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Could not Base64-decode signature."); + handleInvalidResponse(); + return; + } + + // Parse and validate response. + try { + data = ResponseData.parse(signedData); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not parse response."); + handleInvalidResponse(); + return; + } + + if (data.responseCode != responseCode) { + Log.e(TAG, "Response codes don't match."); + handleInvalidResponse(); + return; + } + + if (data.nonce != mNonce) { + Log.e(TAG, "Nonce doesn't match."); + handleInvalidResponse(); + return; + } + + if (!data.packageName.equals(mPackageName)) { + Log.e(TAG, "Package name doesn't match."); + handleInvalidResponse(); + return; + } + + if (!data.versionCode.equals(mVersionCode)) { + Log.e(TAG, "Version codes don't match."); + handleInvalidResponse(); + return; + } + + // Application-specific user identifier. + userId = data.userId; + if (TextUtils.isEmpty(userId)) { + Log.e(TAG, "User identifier is empty."); + handleInvalidResponse(); + return; + } + } + + switch (responseCode) { + case LICENSED: + case LICENSED_OLD_KEY: + int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); + handleResponse(limiterResponse, data); + break; + case NOT_LICENSED: + handleResponse(Policy.NOT_LICENSED, data); + break; + case ERROR_CONTACTING_SERVER: + Log.w(TAG, "Error contacting licensing server."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_SERVER_FAILURE: + Log.w(TAG, "An error has occurred on the licensing server."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_OVER_QUOTA: + Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_INVALID_PACKAGE_NAME: + handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); + break; + case ERROR_NON_MATCHING_UID: + handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); + break; + case ERROR_NOT_MARKET_MANAGED: + handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); + break; + default: + Log.e(TAG, "Unknown response code for license check."); + handleInvalidResponse(); + } + } + + /** + * Confers with policy and calls appropriate callback method. + * + * @param response + * @param rawData + */ + private void handleResponse(int response, ResponseData rawData) { + // Update policy data and increment retry counter (if needed) + mPolicy.processServerResponse(response, rawData); + + // Given everything we know, including cached data, ask the policy if we should grant + // access. + if (mPolicy.allowAccess()) { + mCallback.allow(response); + } else { + mCallback.dontAllow(response); + } + } + + private void handleApplicationError(int code) { + mCallback.applicationError(code); + } + + private void handleInvalidResponse() { + mCallback.dontAllow(Policy.NOT_LICENSED); + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java index a43e454228..d87af3153f 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/NullDeviceLimiter.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -26,7 +26,7 @@ package com.google.android.vending.licensing; */ public class NullDeviceLimiter implements DeviceLimiter { - public int isDeviceAllowed(String userId) { - return Policy.LICENSED; - } + public int isDeviceAllowed(String userId) { + return Policy.LICENSED; + } } diff --git a/platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java index 8731d03aa6..008c150a8e 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/Obfuscator.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java @@ -27,16 +27,16 @@ package com.google.android.vending.licensing; */ public interface Obfuscator { - /** + /** * Obfuscate a string that is being stored into shared preferences. * * @param original The data that is to be obfuscated. * @param key The key for the data that is to be obfuscated. * @return A transformed version of the original data. */ - String obfuscate(String original, String key); + String obfuscate(String original, String key); - /** + /** * Undo the transformation applied to data by the obfuscate() method. * * @param obfuscated The data that is to be un-obfuscated. @@ -44,5 +44,5 @@ public interface Obfuscator { * @return The original data transformed by the obfuscate() method. * @throws ValidationException Optionally thrown if a data integrity check fails. */ - String unobfuscate(String obfuscated, String key) throws ValidationException; + String unobfuscate(String obfuscated, String key) throws ValidationException; } diff --git a/platform/android/java/src/com/google/android/vending/licensing/Policy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java index 65202aceb9..b672a078b7 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/Policy.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java @@ -22,27 +22,27 @@ package com.google.android.vending.licensing; */ public interface Policy { - /** + /** * Change these values to make it more difficult for tools to automatically * strip LVL protection from your APK. */ - /** + /** * LICENSED means that the server returned back a valid license response */ - public static final int LICENSED = 0x0100; - /** + public static final int LICENSED = 0x0100; + /** * NOT_LICENSED means that the server returned back a valid license response * that indicated that the user definitively is not licensed */ - public static final int NOT_LICENSED = 0x0231; - /** + public static final int NOT_LICENSED = 0x0231; + /** * RETRY means that the license response was unable to be determined --- * perhaps as a result of faulty networking */ - public static final int RETRY = 0x0123; + public static final int RETRY = 0x0123; - /** + /** * Provide results from contact with the license server. Retry counts are * incremented if the current value of response is RETRY. Results will be * used for any future policy decisions. @@ -50,16 +50,16 @@ public interface Policy { * @param response the result from validating the server response * @param rawData the raw server response data, can be null for RETRY */ - void processServerResponse(int response, ResponseData rawData); + void processServerResponse(int response, ResponseData rawData); - /** + /** * Check if the user should be allowed access to the application. */ - boolean allowAccess(); + boolean allowAccess(); - /** + /** * Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g. * buy app on the Play Store). */ - String getLicensingUrl(); + String getLicensingUrl(); } diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java new file mode 100644 index 0000000000..feb579af04 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.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(); + // -- GODOT start -- + mEditor.apply(); + // -- GODOT end -- + } + String obfuscatedValue = mObfuscator.obfuscate(value, key); + mEditor.putString(key, obfuscatedValue); + } + + public String getString(String key, String defValue) { + String result; + String value = mPreferences.getString(key, null); + if (value != null) { + try { + result = mObfuscator.unobfuscate(value, key); + } catch (ValidationException e) { + // Unable to unobfuscate, data corrupt or tampered + Log.w(TAG, "Validation error while reading preference: " + key); + result = defValue; + } + } else { + // Preference not found + result = defValue; + } + return result; + } + + public void commit() { + if (mEditor != null) { + mEditor.commit(); + mEditor = null; + } + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java new file mode 100644 index 0000000000..3b5d557e76 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.text.TextUtils; + +import java.util.regex.Pattern; + +/** + * ResponseData from licensing server. + */ +public class ResponseData { + + public int responseCode; + public int nonce; + public String packageName; + public String versionCode; + public String userId; + public long timestamp; + /** Response-specific data. */ + public String extra; + + /** + * Parses response string into ResponseData. + * + * @param responseData response data string + * @throws IllegalArgumentException upon parsing error + * @return ResponseData object + */ + public static ResponseData parse(String responseData) { + // Must parse out main response data and response-specific data. + int index = responseData.indexOf(':'); + String mainData, extraData; + if (-1 == index) { + mainData = responseData; + extraData = ""; + } else { + mainData = responseData.substring(0, index); + extraData = index >= responseData.length() ? "" : responseData.substring(index + 1); + } + + String[] fields = TextUtils.split(mainData, Pattern.quote("|")); + if (fields.length < 6) { + throw new IllegalArgumentException("Wrong number of fields."); + } + + ResponseData data = new ResponseData(); + data.extra = extraData; + data.responseCode = Integer.parseInt(fields[0]); + data.nonce = Integer.parseInt(fields[1]); + data.packageName = fields[2]; + data.versionCode = fields[3]; + // Application-specific user identifier. + data.userId = fields[4]; + data.timestamp = Long.parseLong(fields[5]); + + return data; + } + + @Override + public String toString() { + return TextUtils.join("|", new Object[] { + responseCode, nonce, packageName, versionCode, + userId, timestamp + }); + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000000..e2f0bfdca8 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.vending.licensing.util.URIQueryDecoder; + +/** + * Default policy. All policy decisions are based off of response data received + * from the licensing service. Specifically, the licensing server sends the + * following information: response validity period, error retry period, + * error retry count and a URL for restoring app access in unlicensed cases. + * <p> + * These values will vary based on the the way the application is configured in + * the Google Play publishing console, such as whether the application is + * marked as free or is within its refund period, as well as how often an + * application is checking with the licensing service. + * <p> + * Developers who need more fine grained control over their application's + * licensing policy should implement a custom Policy. + */ +public class ServerManagedPolicy implements Policy { + + private static final String TAG = "ServerManagedPolicy"; + private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy"; + private static final String PREF_LAST_RESPONSE = "lastResponse"; + private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; + private static final String PREF_RETRY_UNTIL = "retryUntil"; + private static final String PREF_MAX_RETRIES = "maxRetries"; + private static final String PREF_RETRY_COUNT = "retryCount"; + private static final String PREF_LICENSING_URL = "licensingUrl"; + private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; + private static final String DEFAULT_RETRY_UNTIL = "0"; + private static final String DEFAULT_MAX_RETRIES = "0"; + private static final String DEFAULT_RETRY_COUNT = "0"; + + private static final long MILLIS_PER_MINUTE = 60 * 1000; + + private long mValidityTimestamp; + private long mRetryUntil; + private long mMaxRetries; + private long mRetryCount; + private long mLastResponseTime = 0; + private int mLastResponse; + private String mLicensingUrl; + private PreferenceObfuscator mPreferences; + + /** + * @param context The context for the current application + * @param obfuscator An obfuscator to be used with preferences. + */ + public ServerManagedPolicy(Context context, Obfuscator obfuscator) { + // Import old values + SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); + mPreferences = new PreferenceObfuscator(sp, obfuscator); + mLastResponse = Integer.parseInt( + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); + mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, + DEFAULT_VALIDITY_TIMESTAMP)); + mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); + mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); + mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); + mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); + } + + /** + * Process a new response from the license server. + * <p> + * This data will be used for computing future policy decisions. The + * following parameters are processed: + * <ul> + * <li>VT: the timestamp that the client should consider the response valid + * until + * <li>GT: the timestamp that the client should ignore retry errors until + * <li>GR: the number of retry errors that the client should ignore + * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. + * buy app on the Play Store) + * </ul> + * + * @param response the result from validating the server response + * @param rawData the raw server response data + */ + public void processServerResponse(int response, ResponseData rawData) { + + // Update retry counter + if (response != Policy.RETRY) { + setRetryCount(0); + } else { + setRetryCount(mRetryCount + 1); + } + + // Update server policy data + Map<String, String> extras = decodeExtras(rawData); + if (response == Policy.LICENSED) { + mLastResponse = response; + // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. + setLicensingUrl(null); + setValidityTimestamp(extras.get("VT")); + setRetryUntil(extras.get("GT")); + setMaxRetries(extras.get("GR")); + } else if (response == Policy.NOT_LICENSED) { + // Clear out stale retry params + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + // Update the licensing URL + setLicensingUrl(extras.get("LU")); + } + + setLastResponse(response); + mPreferences.commit(); + } + + /** + * Set the last license response received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param l the response + */ + private void setLastResponse(int l) { + mLastResponseTime = System.currentTimeMillis(); + mLastResponse = l; + mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); + } + + /** + * Set the current retry count and add to preferences. You must manually + * call PreferenceObfuscator.commit() to commit these changes to disk. + * + * @param c the new retry count + */ + private void setRetryCount(long c) { + mRetryCount = c; + mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); + } + + public long getRetryCount() { + return mRetryCount; + } + + /** + * Set the last validity timestamp (VT) received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param validityTimestamp the VT string received + */ + private void setValidityTimestamp(String validityTimestamp) { + Long lValidityTimestamp; + try { + lValidityTimestamp = Long.parseLong(validityTimestamp); + } catch (NumberFormatException e) { + // No response or not parsable, expire in one minute. + Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); + lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; + validityTimestamp = Long.toString(lValidityTimestamp); + } + + mValidityTimestamp = lValidityTimestamp; + mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); + } + + public long getValidityTimestamp() { + return mValidityTimestamp; + } + + /** + * Set the retry until timestamp (GT) received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param retryUntil the GT string received + */ + private void setRetryUntil(String retryUntil) { + Long lRetryUntil; + try { + lRetryUntil = Long.parseLong(retryUntil); + } catch (NumberFormatException e) { + // No response or not parsable, expire immediately + Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); + retryUntil = "0"; + lRetryUntil = 0l; + } + + mRetryUntil = lRetryUntil; + mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); + } + + public long getRetryUntil() { + return mRetryUntil; + } + + /** + * Set the max retries value (GR) as received from the server and add to + * preferences. You must manually call PreferenceObfuscator.commit() to + * commit these changes to disk. + * + * @param maxRetries the GR string received + */ + private void setMaxRetries(String maxRetries) { + Long lMaxRetries; + try { + lMaxRetries = Long.parseLong(maxRetries); + } catch (NumberFormatException e) { + // No response or not parsable, expire immediately + Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); + maxRetries = "0"; + lMaxRetries = 0l; + } + + mMaxRetries = lMaxRetries; + mPreferences.putString(PREF_MAX_RETRIES, maxRetries); + } + + public long getMaxRetries() { + return mMaxRetries; + } + + /** + * Set the license URL value (LU) as received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. + * + * @param url the LU string received + */ + private void setLicensingUrl(String url) { + mLicensingUrl = url; + mPreferences.putString(PREF_LICENSING_URL, url); + } + + public String getLicensingUrl() { + return mLicensingUrl; + } + + /** + * {@inheritDoc} + * + * This implementation allows access if either:<br> + * <ol> + * <li>a LICENSED response was received within the validity period + * <li>a RETRY response was received in the last minute, and we are under + * the RETRY count or in the RETRY period. + * </ol> + */ + public boolean allowAccess() { + long ts = System.currentTimeMillis(); + if (mLastResponse == Policy.LICENSED) { + // Check if the LICENSED response occurred within the validity timeout. + if (ts <= mValidityTimestamp) { + // Cached LICENSED response is still valid. + return true; + } + } else if (mLastResponse == Policy.RETRY && + ts < mLastResponseTime + MILLIS_PER_MINUTE) { + // Only allow access if we are within the retry period or we haven't used up our + // max retries. + return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); + } + return false; + } + + private Map<String, String> decodeExtras( + com.google.android.vending.licensing.ResponseData rawData) { + Map<String, String> results = new HashMap<String, String>(); + if (rawData == null) { + return results; + } + + try { + URI rawExtras = new URI("?" + rawData.extra); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } + +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java index 9849730c38..c2d55c37f1 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/StrictPolicy.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java @@ -38,18 +38,18 @@ import java.util.Map; */ public class StrictPolicy implements Policy { - private static final String TAG = "StrictPolicy"; + private static final String TAG = "StrictPolicy"; - private int mLastResponse; - private String mLicensingUrl; + private int mLastResponse; + private String mLicensingUrl; - public StrictPolicy() { - // Set default policy. This will force the application to check the policy on launch. - mLastResponse = Policy.RETRY; - mLicensingUrl = null; - } + public StrictPolicy() { + // Set default policy. This will force the application to check the policy on launch. + mLastResponse = Policy.RETRY; + mLicensingUrl = null; + } - /** + /** * Process a new response from the license server. Since we aren't * performing any caching, this equates to reading the LicenseResponse. * Any cache-related ResponseData is ignored, but the licensing URL @@ -58,42 +58,43 @@ public class StrictPolicy implements Policy { * @param response the result from validating the server response * @param rawData the raw server response data */ - public void processServerResponse(int response, ResponseData rawData) { - mLastResponse = response; + public void processServerResponse(int response, ResponseData rawData) { + mLastResponse = response; - if (response == Policy.NOT_LICENSED) { - Map<String, String> extras = decodeExtras(rawData); - mLicensingUrl = extras.get("LU"); - } - } + if (response == Policy.NOT_LICENSED) { + Map<String, String> extras = decodeExtras(rawData); + mLicensingUrl = extras.get("LU"); + } + } - /** + /** * {@inheritDoc} * * This implementation allows access if and only if a LICENSED response * was received the last time the server was contacted. */ - public boolean allowAccess() { - return (mLastResponse == Policy.LICENSED); - } + public boolean allowAccess() { + return (mLastResponse == Policy.LICENSED); + } - public String getLicensingUrl() { - return mLicensingUrl; - } + 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; - } + 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; + } - try { - URI rawExtras = new URI("?" + rawData.extra); - URIQueryDecoder.DecodeQuery(rawExtras, results); - } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); - } - return results; - } } diff --git a/platform/android/java/src/com/google/android/vending/licensing/ValidationException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java index 79b70e6804..ee4df47c68 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/ValidationException.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java @@ -21,13 +21,13 @@ package com.google.android.vending.licensing; * {@link Obfuscator}.} */ public class ValidationException extends Exception { - public ValidationException() { - super(); - } + public ValidationException() { + super(); + } - public ValidationException(String s) { - super(s); - } + public ValidationException(String s) { + super(s); + } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; } diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java new file mode 100644 index 0000000000..79efca9621 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java @@ -0,0 +1,578 @@ +// 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 + */ + +// -- GODOT start -- +import org.godotengine.godot.BuildConfig; +// -- GODOT end -- + +/** + * 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; + } + + // -- GODOT start -- + //assert (e == outBuff.length); + if (BuildConfig.DEBUG && e != outBuff.length) + throw new RuntimeException(); + // -- GODOT end -- + return outBuff; + } + + + /* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array <var>source</var> + * and writes the resulting bytes (up to three of them) + * to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 4 for + * the <var>source</var> array or <var>destOffset</var> + 3 for + * the <var>destination</var> array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded data. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded byte array. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = 0; i < len; i++) { + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte) (source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) + || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + + ": " + source[i + off] + "(decimal)"); + } + } + + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + } + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } +} diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java index 50724a9b05..1aef1b54b8 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64DecoderException.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -20,13 +20,13 @@ package com.google.android.vending.licensing.util; * @author nelson */ public class Base64DecoderException extends Exception { - public Base64DecoderException() { - super(); - } + public Base64DecoderException() { + super(); + } - public Base64DecoderException(String s) { - super(s); - } + public Base64DecoderException(String s) { + super(s); + } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; } diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java index 4f908b472c..5155bf5ac3 100644 --- a/platform/android/java/src/com/google/android/vending/licensing/util/URIQueryDecoder.java +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java @@ -25,36 +25,36 @@ import java.util.Map; import java.util.Scanner; public class URIQueryDecoder { - private static final String TAG = "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."); - } - } + static public void DecodeQuery(URI encodedURI, Map<String, String> results) { + Scanner scanner = new Scanner(encodedURI.getRawQuery()); + scanner.useDelimiter("&"); + try { + while (scanner.hasNext()) { + String param = scanner.next(); + String[] valuePair = param.split("="); + String name, value; + if (valuePair.length == 1) { + value = null; + } else if (valuePair.length == 2) { + value = URLDecoder.decode(valuePair[1], "UTF-8"); + } else { + throw new IllegalArgumentException("query parameter invalid"); + } + name = URLDecoder.decode(valuePair[0], "UTF-8"); + results.put(name, value); + } + } catch (UnsupportedEncodingException e) { + // This should never happen. + Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error."); + } + } } diff --git a/platform/android/java/src/org/godotengine/godot/Dictionary.java b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java index 588d9ae646..588d9ae646 100644 --- a/platform/android/java/src/org/godotengine/godot/Dictionary.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java diff --git a/platform/android/java/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 6e1841fa8b..739aa285bf 100644 --- a/platform/android/java/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -31,6 +31,7 @@ package org.godotengine.godot; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; @@ -56,7 +57,10 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Messenger; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.provider.Settings.Secure; +import android.support.annotation.Keep; import android.support.v4.content.ContextCompat; import android.view.Display; import android.view.KeyEvent; @@ -93,13 +97,13 @@ import org.godotengine.godot.input.GodotEditText; import org.godotengine.godot.payments.PaymentsManager; import org.godotengine.godot.xr.XRMode; -public class Godot extends Activity implements SensorEventListener, IDownloaderClient { +public abstract class Godot extends Activity implements SensorEventListener, IDownloaderClient { static final int MAX_SINGLETONS = 64; static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; static final int REQUEST_CAMERA_PERMISSION = 2; + static final int REQUEST_VIBRATE_PERMISSION = 3; private IStub mDownloaderClientStub; - private IDownloaderService mRemoteService; private TextView mStatusText; private TextView mProgressFraction; private TextView mProgressPercent; @@ -142,8 +146,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC private void setButtonPausedState(boolean paused) { mStatePaused = paused; - int stringResourceID = paused ? com.godot.game.R.string.text_button_resume : - com.godot.game.R.string.text_button_pause; + int stringResourceID = paused ? R.string.text_button_resume : + R.string.text_button_pause; mPauseButton.setText(stringResourceID); } @@ -222,15 +226,9 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC private Sensor mMagnetometer; private Sensor mGyroscope; - public FrameLayout layout; - public static GodotIO io; - public static void setWindowTitle(String title) { - //setTitle(title); - } - - static SingletonBase singletons[] = new SingletonBase[MAX_SINGLETONS]; + static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS]; static int singleton_count = 0; public interface ResultCallback { @@ -266,13 +264,14 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } }; - public void onVideoInit() { + /** + * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer. + */ + @Keep + private void onVideoInit() { boolean use_gl3 = getGLESVersionCode() >= 0x00030000; - //mView = new GodotView(getApplication(),io,use_gl3); - //setContentView(mView); - - layout = new FrameLayout(this); + final FrameLayout layout = new FrameLayout(this); layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setContentView(layout); @@ -324,6 +323,26 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC }); } + /** + * Used by the native code (java_godot_wrapper.h) to vibrate the device. + * @param durationMs + */ + @SuppressLint("MissingPermission") + @Keep + private void vibrate(int durationMs) { + if (requestPermission("VIBRATE")) { + Vibrator v = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); + if (v != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)); + } else { + //deprecated in API 26 + v.vibrate(durationMs); + } + } + } + } + public void restart() { // HACK: // @@ -338,7 +357,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC // Bundle args = new Bundle(); args.putParcelable("intent", mCurrentIntent); - startInstrumentation(new ComponentName(Godot.this, GodotInstrumentation.class), null, args); + startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args); } public void alert(final String message, final String title) { @@ -405,6 +424,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC /** * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused. */ + @Keep private boolean isActivityResumed() { return activityResumed; } @@ -412,10 +432,20 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC /** * Used by the native code (java_godot_wrapper.h) to access the Android surface. */ + @Keep private Surface getSurface() { return mView.getHolder().getSurface(); } + /** + * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. + * @return The input fallback mapping for the current XR mode. + */ + @Keep + private String getInputFallbackMapping() { + return xrMode.inputFallbackMapping; + } + String expansion_pack_path; private void initializeGodot() { @@ -463,8 +493,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC @Override public void onServiceConnected(Messenger m) { - mRemoteService = DownloaderServiceMarshaller.CreateProxy(m); - mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); + IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); + remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); } @Override @@ -472,7 +502,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC super.onCreate(icicle); 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); mClipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); @@ -585,20 +614,19 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, GodotDownloaderService.class); - setContentView(com.godot.game.R.layout.downloading_expansion); - mPB = (ProgressBar)findViewById(com.godot.game.R.id.progressBar); - mStatusText = (TextView)findViewById(com.godot.game.R.id.statusText); - mProgressFraction = (TextView)findViewById(com.godot.game.R.id.progressAsFraction); - mProgressPercent = (TextView)findViewById(com.godot.game.R.id.progressAsPercentage); - mAverageSpeed = (TextView)findViewById(com.godot.game.R.id.progressAverageSpeed); - mTimeRemaining = (TextView)findViewById(com.godot.game.R.id.progressTimeRemaining); - mDashboard = findViewById(com.godot.game.R.id.downloaderDashboard); - mCellMessage = findViewById(com.godot.game.R.id.approveCellular); - mPauseButton = (Button)findViewById(com.godot.game.R.id.pauseButton); - mWiFiSettingsButton = (Button)findViewById(com.godot.game.R.id.wifiSettingsButton); + setContentView(R.layout.downloading_expansion); + mPB = (ProgressBar)findViewById(R.id.progressBar); + mStatusText = (TextView)findViewById(R.id.statusText); + mProgressFraction = (TextView)findViewById(R.id.progressAsFraction); + mProgressPercent = (TextView)findViewById(R.id.progressAsPercentage); + mAverageSpeed = (TextView)findViewById(R.id.progressAverageSpeed); + mTimeRemaining = (TextView)findViewById(R.id.progressTimeRemaining); + mDashboard = findViewById(R.id.downloaderDashboard); + mCellMessage = findViewById(R.id.approveCellular); + mPauseButton = (Button)findViewById(R.id.pauseButton); + mWiFiSettingsButton = (Button)findViewById(R.id.wifiSettingsButton); return; - } else { } } catch (NameNotFoundException e) { // TODO Auto-generated catch block @@ -610,8 +638,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC mCurrentIntent = getIntent(); initializeGodot(); - - //instanceSingleton( new GodotFacebook(this) ); } @Override @@ -625,6 +651,10 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC GodotLib.ondestroy(this); super.onDestroy(); + + // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each + // native Godot components that is started in Godot#onVideoInit. + forceQuit(); } @Override @@ -639,12 +669,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC return; } mView.onPause(); - mView.queueEvent(new Runnable() { - @Override - public void run() { - GodotLib.focusout(); - } - }); + mSensorManager.unregisterListener(this); for (int i = 0; i < singleton_count; i++) { @@ -673,6 +698,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC @Override protected void onResume() { super.onResume(); + activityResumed = true; if (!godot_initialized) { if (null != mDownloaderClientStub) { mDownloaderClientStub.connect(this); @@ -681,12 +707,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } mView.onResume(); - mView.queueEvent(new Runnable() { - @Override - public void run() { - GodotLib.focusin(); - } - }); + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); @@ -707,8 +728,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC singletons[i].onMainResume(); } - - activityResumed = true; } public void UiChangeListener() { @@ -816,8 +835,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } } - public void forceQuit() { - + private void forceQuit() { System.exit(0); } @@ -864,7 +882,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC } } - //@Override public boolean dispatchTouchEvent (MotionEvent event) { public boolean gotTouchEvent(final MotionEvent event) { final int evcount = event.getPointerCount(); @@ -935,8 +952,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC for (int i = cc.length; --i >= 0; cnt += cc[i] != 0 ? 1 : 0) ; if (cnt == 0) return super.onKeyMultiple(inKeyCode, repeatCount, event); - final Activity me = this; - queueEvent(new Runnable() { + mView.queueEvent(new Runnable() { // This method will be called on the rendering thread: public void run() { for (int i = 0, n = cc.length; i < n; i++) { @@ -952,20 +968,10 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC return true; } - private void queueEvent(Runnable runnable) { - // TODO Auto-generated method stub - } - public PaymentsManager getPaymentsManager() { return mPaymentsManager; } - /* - public void setPaymentsManager(PaymentsManager mPaymentsManager) { - this.mPaymentsManager = mPaymentsManager; - } - */ - public boolean requestPermission(String p_name) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already @@ -985,6 +991,13 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC return false; } } + + if (p_name.equals("VIBRATE")) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); + return false; + } + } return true; } @@ -1003,7 +1016,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC switch (newState) { case IDownloaderClient.STATE_IDLE: // STATE_IDLE means the service is listening, so it's - // safe to start making calls via mRemoteService. + // safe to start making remote service calls. paused = false; indeterminate = true; break; @@ -1070,9 +1083,9 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC @Override public void onDownloadProgress(DownloadProgressInfo progress) { - mAverageSpeed.setText(getString(com.godot.game.R.string.kilobytes_per_second, + mAverageSpeed.setText(getString(R.string.kilobytes_per_second, Helpers.getSpeedString(progress.mCurrentSpeed))); - mTimeRemaining.setText(getString(com.godot.game.R.string.time_remaining, + mTimeRemaining.setText(getString(R.string.time_remaining, Helpers.getTimeRemaining(progress.mTimeRemaining))); mPB.setMax((int)(progress.mOverallTotal >> 8)); diff --git a/platform/android/java/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java index e7e2a3f808..e7e2a3f808 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java diff --git a/platform/android/java/src/org/godotengine/godot/GodotDownloaderService.java b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java index 8e10710c9f..8e10710c9f 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotDownloaderService.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java diff --git a/platform/android/java/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 98174157ec..04566cf62c 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -29,26 +29,18 @@ /*************************************************************************/ package org.godotengine.godot; -import android.app.*; import android.content.*; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; -import android.graphics.*; -import android.hardware.*; import android.media.*; import android.net.Uri; import android.os.*; -import android.text.*; -import android.text.method.*; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; -import android.view.*; -import android.view.inputmethod.InputMethodManager; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; import java.util.Locale; import org.godotengine.godot.input.*; //android.os.Build diff --git a/platform/android/java/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java index 0466f380e8..0466f380e8 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotInstrumentation.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java new file mode 100644 index 0000000000..67dce172dc --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -0,0 +1,231 @@ +/*************************************************************************/ +/* GodotLib.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2019 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; + +import android.app.Activity; +import android.hardware.SensorEvent; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Wrapper for native library + */ +public class GodotLib { + + public static GodotIO io; + + static { + System.loadLibrary("godot_android"); + } + + /** + * Invoked on the main thread to initialize Godot native layer. + */ + public static native void initialize(Godot p_instance, Object p_asset_manager, boolean use_apk_expansion); + + /** + * Invoked on the main thread to clean up Godot native layer. + * @see Activity#onDestroy() + */ + public static native void ondestroy(Godot p_instance); + + /** + * Invoked on the GL thread to complete setup for the Godot native layer logic. + * @param p_cmdline Command line arguments used to configure Godot native layer components. + */ + public static native void setup(String[] p_cmdline); + + /** + * Invoked on the GL thread when the underlying Android surface has changed size. + * @param width + * @param height + * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) + */ + public static native void resize(int width, int height); + + /** + * Invoked on the GL thread when the underlying Android surface is created or recreated. + * @param p_32_bits + * @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig) + */ + public static native void newcontext(boolean p_32_bits); + + /** + * Forward {@link Activity#onBackPressed()} event from the main thread to the GL thread. + */ + public static native void back(); + + /** + * Invoked on the GL thread to draw the current frame. + * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10) + */ + public static native void step(); + + /** + * Forward touch events from the main thread to the GL thread. + */ + public static native void touch(int what, int pointer, int howmany, int[] arr); + + /** + * Forward hover events from the main thread to the GL thread. + */ + public static native void hover(int type, int x, int y); + + /** + * Forward accelerometer sensor events from the main thread to the GL thread. + * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) + */ + public static native void accelerometer(float x, float y, float z); + + /** + * Forward gravity sensor events from the main thread to the GL thread. + * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) + */ + public static native void gravity(float x, float y, float z); + + /** + * Forward magnetometer sensor events from the main thread to the GL thread. + * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) + */ + public static native void magnetometer(float x, float y, float z); + + /** + * Forward gyroscope sensor events from the main thread to the GL thread. + * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) + */ + public static native void gyroscope(float x, float y, float z); + + /** + * Forward regular key events from the main thread to the GL thread. + */ + public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed); + + /** + * Forward game device's key events from the main thread to the GL thread. + */ + public static native void joybutton(int p_device, int p_but, boolean p_pressed); + + /** + * Forward joystick devices axis motion events from the main thread to the GL thread. + */ + public static native void joyaxis(int p_device, int p_axis, float p_value); + + /** + * Forward joystick devices hat motion events from the main thread to the GL thread. + */ + public static native void joyhat(int p_device, int p_hat_x, int p_hat_y); + + /** + * Fires when a joystick device is added or removed. + */ + public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name); + + /** + * Invoked when the Android activity resumes. + * @see Activity#onResume() + */ + public static native void focusin(); + + /** + * Invoked when the Android activity pauses. + * @see Activity#onPause() + */ + public static native void focusout(); + + /** + * Invoked when the audio thread is started. + */ + public static native void audio(); + + /** + * Used to setup a {@link org.godotengine.godot.Godot.SingletonBase} instance. + * @param p_name Name of the instance. + * @param p_object Reference to the singleton instance. + */ + public static native void singleton(String p_name, Object p_object); + + /** + * Used to complete registration of the {@link org.godotengine.godot.Godot.SingletonBase} instance's methods. + * @param p_sname Name of the instance + * @param p_name Name of the method to register + * @param p_ret Return type of the registered method + * @param p_params Method parameters types + */ + public static native void method(String p_sname, String p_name, String p_ret, String[] p_params); + + /** + * Used to access Godot global properties. + * @param p_key Property key + * @return String value of the property + */ + public static native String getGlobal(String p_key); + + /** + * Invoke method |p_method| on the Godot object specified by |p_id| + * @param p_id Id of the Godot object to invoke + * @param p_method Name of the method to invoke + * @param p_params Parameters to use for method invocation + */ + public static native void callobject(int p_id, String p_method, Object[] p_params); + + /** + * Invoke method |p_method| on the Godot object specified by |p_id| during idle time. + * @param p_id Id of the Godot object to invoke + * @param p_method Name of the method to invoke + * @param p_params Parameters to use for method invocation + */ + public static native void calldeferred(int p_id, String p_method, Object[] p_params); + + /** + * Forward the results from a permission request. + * @see Activity#onRequestPermissionsResult(int, String[], int[]) + * @param p_permission Request permission + * @param p_result True if the permission was granted, false otherwise + */ + public static native void requestPermissionResult(String p_permission, boolean p_result); + + /** + * Invoked on the GL thread to configure the height of the virtual keyboard. + */ + public static native void setVirtualKeyboardHeight(int p_height); + + /** + * Invoked on the GL thread when the {@link GodotRenderer} has been resumed. + * @see GodotRenderer#onActivityResumed() + */ + public static native void onRendererResumed(); + + /** + * Invoked on the GL thread when the {@link GodotRenderer} has been paused. + * @see GodotRenderer#onActivityPaused() + */ + public static native void onRendererPaused(); +} diff --git a/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java b/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java index 1432cd3a67..1432cd3a67 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotPaymentV3.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java diff --git a/platform/android/java/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java index 8e3775c2a9..56ba88656e 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java @@ -40,7 +40,14 @@ import org.godotengine.godot.utils.GLUtils; */ class GodotRenderer implements GLSurfaceView.Renderer { + private boolean activityJustResumed = false; + public void onDrawFrame(GL10 gl) { + if (activityJustResumed) { + GodotLib.onRendererResumed(); + activityJustResumed = false; + } + GodotLib.step(); for (int i = 0; i < Godot.singleton_count; i++) { Godot.singletons[i].onGLDrawFrame(gl); @@ -58,4 +65,14 @@ class GodotRenderer implements GLSurfaceView.Renderer { public void onSurfaceCreated(GL10 gl, EGLConfig config) { GodotLib.newcontext(GLUtils.use_32); } + + void onActivityResumed() { + // We defer invoking GodotLib.onRendererResumed() until the first draw frame call. + // This ensures we have a valid GL context and surface when we do so. + activityJustResumed = true; + } + + void onActivityPaused() { + GodotLib.onRendererPaused(); + } } diff --git a/platform/android/java/src/org/godotengine/godot/GodotView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java index fc3e47e69d..5511e5d782 100644 --- a/platform/android/java/src/org/godotengine/godot/GodotView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java @@ -68,6 +68,7 @@ public class GodotView extends GLSurfaceView { private final Godot activity; private final GodotInputHandler inputHandler; + private final GodotRenderer godotRenderer; public GodotView(Godot activity, XRMode xrMode, boolean p_use_gl3, boolean p_use_32_bits, boolean p_use_debug_opengl) { super(activity); @@ -77,6 +78,7 @@ public class GodotView extends GLSurfaceView { this.activity = activity; this.inputHandler = new GodotInputHandler(this); + this.godotRenderer = new GodotRenderer(); init(xrMode, false, 16, 0); } @@ -161,10 +163,38 @@ public class GodotView extends GLSurfaceView { } /* Set the renderer responsible for frame rendering */ - setRenderer(new GodotRenderer()); + setRenderer(godotRenderer); } public void onBackPressed() { activity.onBackPressed(); } + + @Override + public void onResume() { + super.onResume(); + + queueEvent(new Runnable() { + @Override + public void run() { + // Resume the renderer + godotRenderer.onActivityResumed(); + GodotLib.focusin(); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.focusout(); + // Pause the renderer + godotRenderer.onActivityPaused(); + } + }); + } } diff --git a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index 45b739baa0..45b739baa0 100644 --- a/platform/android/java/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java diff --git a/platform/android/java/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index a443a0ad90..2756ca6c83 100644 --- a/platform/android/java/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -96,7 +96,6 @@ public class GodotInputHandler implements InputDeviceListener { GodotLib.joybutton(device_id, button, false); } }); - return true; } } else { final int chr = event.getUnicodeChar(0); @@ -108,7 +107,7 @@ public class GodotInputHandler implements InputDeviceListener { }); }; - return false; + return true; } public boolean onKeyDown(final int keyCode, KeyEvent event) { @@ -142,7 +141,6 @@ public class GodotInputHandler implements InputDeviceListener { GodotLib.joybutton(device_id, button, true); } }); - return true; } } else { final int chr = event.getUnicodeChar(0); @@ -154,7 +152,7 @@ public class GodotInputHandler implements InputDeviceListener { }); }; - return false; + return true; } public boolean onGenericMotionEvent(MotionEvent event) { @@ -190,7 +188,18 @@ public class GodotInputHandler implements InputDeviceListener { } return true; } - }; + } else if ((event.getSource() & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS) { + final int x = Math.round(event.getX()); + final int y = Math.round(event.getY()); + final int type = event.getAction(); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.hover(type, x, y); + } + }); + return true; + } return false; } diff --git a/platform/android/java/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index d6e7ad5b18..9b372c75e3 100644 --- a/platform/android/java/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -32,7 +32,6 @@ package org.godotengine.godot.input; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java index 4042c42e9d..4042c42e9d 100644 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerCompat.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java diff --git a/platform/android/java/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java index e4bafa7ff9..e4bafa7ff9 100644 --- a/platform/android/java/src/org/godotengine/godot/input/InputManagerV16.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java diff --git a/platform/android/java/src/org/godotengine/godot/input/Joystick.java b/platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java index ff95bfb0c5..ff95bfb0c5 100644 --- a/platform/android/java/src/org/godotengine/godot/input/Joystick.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java diff --git a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java index f872e7af56..4c1050c948 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ConsumeTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java @@ -33,7 +33,6 @@ package org.godotengine.godot.payments; import android.content.Context; import android.os.AsyncTask; import android.os.RemoteException; -import android.util.Log; import com.android.vending.billing.IInAppBillingService; import java.lang.ref.WeakReference; diff --git a/platform/android/java/src/org/godotengine/godot/payments/HandlePurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java index 5424ebb49d..1a914967a2 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/HandlePurchaseTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java @@ -31,18 +31,7 @@ package org.godotengine.godot.payments; import android.app.Activity; -import android.app.PendingIntent; -import android.app.ProgressDialog; -import android.content.Context; import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.Log; -import com.android.vending.billing.IInAppBillingService; -import org.godotengine.godot.GodotLib; -import org.godotengine.godot.utils.Crypt; import org.json.JSONException; import org.json.JSONObject; diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java index 8a2facbcfb..8a2facbcfb 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsCache.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java diff --git a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java index a0dbc432c1..c079c55854 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PaymentsManager.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java @@ -43,7 +43,6 @@ import android.util.Log; import com.android.vending.billing.IInAppBillingService; import java.util.ArrayList; import java.util.Arrays; -import org.godotengine.godot.Godot; import org.godotengine.godot.GodotPaymentV3; import org.json.JSONException; import org.json.JSONObject; diff --git a/platform/android/java/src/org/godotengine/godot/payments/PurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java index 650c5178f0..9adc85e521 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/PurchaseTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java @@ -32,19 +32,12 @@ package org.godotengine.godot.payments; import android.app.Activity; import android.app.PendingIntent; -import android.app.ProgressDialog; -import android.content.Context; import android.content.Intent; import android.content.IntentSender.SendIntentException; -import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; import com.android.vending.billing.IInAppBillingService; -import org.godotengine.godot.GodotLib; -import org.godotengine.godot.utils.Crypt; -import org.json.JSONException; -import org.json.JSONObject; abstract public class PurchaseTask { diff --git a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java index daca6ef5ae..daca6ef5ae 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java diff --git a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java index d32c80e8e0..17a2a197ad 100644 --- a/platform/android/java/src/org/godotengine/godot/payments/ValidateTask.java +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java @@ -31,21 +31,10 @@ package org.godotengine.godot.payments; import android.app.Activity; -import android.app.PendingIntent; import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; import android.os.AsyncTask; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.Log; -import com.android.vending.billing.IInAppBillingService; import java.lang.ref.WeakReference; -import org.godotengine.godot.Godot; -import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotPaymentV3; -import org.godotengine.godot.utils.Crypt; import org.godotengine.godot.utils.HttpRequester; import org.godotengine.godot.utils.RequestParams; import org.json.JSONException; diff --git a/platform/android/java/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java index 4c551d1d21..4c551d1d21 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/Crypt.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java diff --git a/platform/android/java/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java b/platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java index b61007faa3..b61007faa3 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java diff --git a/platform/android/java/src/org/godotengine/godot/utils/GLUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java index 6c95494f8b..6c95494f8b 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/GLUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java diff --git a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java b/platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java index e98f533c23..02ae753b3e 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/HttpRequester.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java @@ -39,12 +39,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.security.KeyStore; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; -import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; @@ -58,7 +55,6 @@ import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; diff --git a/platform/android/java/src/org/godotengine/godot/utils/RequestParams.java b/platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java index b9fe0dd0c9..b9fe0dd0c9 100644 --- a/platform/android/java/src/org/godotengine/godot/utils/RequestParams.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/XRMode.java b/platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java index dd5701af7d..5896b23ac3 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/XRMode.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java @@ -34,16 +34,18 @@ package org.godotengine.godot.xr; * Godot available XR modes. */ public enum XRMode { - REGULAR(0, "Regular", "--xr_mode_regular"), // Regular/flatscreen - OVR(1, "Oculus Mobile VR", "--xr_mode_ovr"); + REGULAR(0, "Regular", "--xr_mode_regular", "Default Android Gamepad"), // Regular/flatscreen + OVR(1, "Oculus Mobile VR", "--xr_mode_ovr", ""); final int index; final String label; public final String cmdLineArg; + public final String inputFallbackMapping; - XRMode(int index, String label, String cmdLineArg) { + XRMode(int index, String label, String cmdLineArg, String inputFallbackMapping) { this.index = index; this.label = label; this.cmdLineArg = cmdLineArg; + this.inputFallbackMapping = inputFallbackMapping; } } diff --git a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java index ff836a31ca..ff836a31ca 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java index 5f6da8c672..5f6da8c672 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java index f1e38c35d8..f1e38c35d8 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java index 3836967f86..3836967f86 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index 4f1e9a696b..4f1e9a696b 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java diff --git a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java index f5718ef2b3..f5718ef2b3 100644 --- a/platform/android/java/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java diff --git a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differdeleted file mode 100644 index 2c246b04a4..0000000000 --- a/platform/android/java/res/drawable-hdpi/notify_panel_notification_icon_bg.png +++ /dev/null diff --git a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differdeleted file mode 100644 index 8bcd464bed..0000000000 --- a/platform/android/java/res/drawable-mdpi/notify_panel_notification_icon_bg.png +++ /dev/null diff --git a/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png Binary files differdeleted file mode 100644 index b458ff3057..0000000000 --- a/platform/android/java/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png +++ /dev/null diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle new file mode 100644 index 0000000000..f6921c70aa --- /dev/null +++ b/platform/android/java/settings.gradle @@ -0,0 +1,5 @@ +// Configure the root project. +rootProject.name = "Godot" + +include ':app' +include ':lib' diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java deleted file mode 100644 index 9a78a6d3df..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java +++ /dev/null @@ -1,78 +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; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * This class contains progress information about the active download(s). - * - * When you build the Activity that initiates a download and tracks the - * progress by implementing the {@link IDownloaderClient} interface, you'll - * receive a DownloadProgressInfo object in each call to the {@link - * IDownloaderClient#onDownloadProgress} method. This allows you to update - * your activity's UI with information about the download progress, such - * as the progress so far, time remaining and current speed. - */ -public class DownloadProgressInfo implements Parcelable { - public long mOverallTotal; - public long mOverallProgress; - public long mTimeRemaining; // time remaining - public float mCurrentSpeed; // speed in KB/S - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel p, int i) { - p.writeLong(mOverallTotal); - p.writeLong(mOverallProgress); - p.writeLong(mTimeRemaining); - p.writeFloat(mCurrentSpeed); - } - - public DownloadProgressInfo(Parcel p) { - mOverallTotal = p.readLong(); - mOverallProgress = p.readLong(); - mTimeRemaining = p.readLong(); - mCurrentSpeed = p.readFloat(); - } - - public DownloadProgressInfo(long overallTotal, long overallProgress, - long timeRemaining, - float currentSpeed) { - this.mOverallTotal = overallTotal; - this.mOverallProgress = overallProgress; - this.mTimeRemaining = timeRemaining; - this.mCurrentSpeed = currentSpeed; - } - - public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { - @Override - public DownloadProgressInfo createFromParcel(Parcel parcel) { - return new DownloadProgressInfo(parcel); - } - - @Override - public DownloadProgressInfo[] newArray(int i) { - return new DownloadProgressInfo[i]; - } - }; -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java deleted file mode 100644 index 146426ef83..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +++ /dev/null @@ -1,290 +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; - -import com.google.android.vending.expansion.downloader.impl.DownloaderService; - -import android.app.PendingIntent; -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.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.util.Log; - -import java.lang.ref.WeakReference; - -/** - * This class binds the service API to your application client. It contains the IDownloaderClient proxy, - * which is used to call functions in your client as well as the Stub, which is used to call functions - * in the client implementation of IDownloaderClient. - * - * <p>The IPC is implemented using an Android Messenger and a service Binder. The connect method - * should be called whenever the client wants to bind to the service. It opens up a service connection - * that ends up calling the onServiceConnected client API that passes the service messenger - * in. If the client wants to be notified by the service, it is responsible for then passing its - * messenger to the service in a separate call. - * - * <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}. - * - * <p>When your application first starts, you should first check whether your app's expansion files are - * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which - * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method - * returns a value indicating whether download is required or not. - * - * <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through - * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link - * IStub} object that you need in order to receive calls through your {@link IDownloaderClient} - * interface. - */ -public class DownloaderClientMarshaller { - public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; - public static final int MSG_ONDOWNLOADPROGRESS = 11; - public static final int MSG_ONSERVICECONNECTED = 12; - - public static final String PARAM_NEW_STATE = "newState"; - public static final String PARAM_PROGRESS = "progress"; - public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; - - public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; - public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; - public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; - - private static class Proxy implements IDownloaderClient { - private Messenger mServiceMessenger; - - @Override - public void onDownloadStateChanged(int newState) { - Bundle params = new Bundle(1); - params.putInt(PARAM_NEW_STATE, newState); - send(MSG_ONDOWNLOADSTATE_CHANGED, params); - } - - @Override - public void onDownloadProgress(DownloadProgressInfo progress) { - Bundle params = new Bundle(1); - params.putParcelable(PARAM_PROGRESS, progress); - send(MSG_ONDOWNLOADPROGRESS, params); - } - - private void send(int method, Bundle params) { - Message m = Message.obtain(null, method); - m.setData(params); - try { - mServiceMessenger.send(m); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - public Proxy(Messenger msg) { - mServiceMessenger = msg; - } - - @Override - public void onServiceConnected(Messenger m) { - /** - * This is never called through the proxy. - */ - } - } - - private static class Stub implements IStub { - private IDownloaderClient mItf = null; - private Class<?> mDownloaderServiceClass; - private boolean mBound; - private Messenger mServiceMessenger; - private Context mContext; - /** - * Target we publish for clients to send messages to IncomingHandler. - */ - private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); - final Messenger mMessenger = new Messenger(mMsgHandler); - - private static class MessengerHandlerClient extends Handler { - private final WeakReference<Stub> mDownloader; - public MessengerHandlerClient(Stub downloader) { - mDownloader = new WeakReference<>(downloader); - } - - @Override - public void handleMessage(Message msg) { - Stub downloader = mDownloader.get(); - if (downloader != null) { - downloader.handleMessage(msg); - } - } - } - - private void handleMessage(Message msg) { - switch (msg.what) { - case MSG_ONDOWNLOADPROGRESS: - Bundle bun = msg.getData(); - if (null != mContext) { - bun.setClassLoader(mContext.getClassLoader()); - DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData() - .getParcelable(PARAM_PROGRESS); - mItf.onDownloadProgress(dpi); - } - break; - case MSG_ONDOWNLOADSTATE_CHANGED: - mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); - break; - case MSG_ONSERVICECONNECTED: - mItf.onServiceConnected( - (Messenger)msg.getData().getParcelable(PARAM_MESSENGER)); - break; - } - } - - public Stub(IDownloaderClient itf, Class<?> downloaderService) { - mItf = itf; - mDownloaderServiceClass = downloaderService; - } - - /** - * Class for interacting with the main interface of the service. - */ - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - mServiceMessenger = new Messenger(service); - mItf.onServiceConnected( - mServiceMessenger); - } - - public void onServiceDisconnected(ComponentName className) { - // This is called when the connection with the service has been - // unexpectedly disconnected -- that is, its process crashed. - mServiceMessenger = null; - } - }; - - @Override - public void connect(Context c) { - mContext = c; - Intent bindIntent = new Intent(c, mDownloaderServiceClass); - bindIntent.putExtra(PARAM_MESSENGER, mMessenger); - if (!c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND)) { - if (Constants.LOGVV) { - Log.d(Constants.TAG, "Service Unbound"); - } - } else { - mBound = true; - } - } - - @Override - public void disconnect(Context c) { - if (mBound) { - c.unbindService(mConnection); - mBound = false; - } - mContext = null; - } - - @Override - public Messenger getMessenger() { - return mMessenger; - } - } - - /** - * Returns a proxy that will marshal calls to IDownloaderClient methods - * - * @param msg - * @return - */ - public static IDownloaderClient CreateProxy(Messenger msg) { - return new Proxy(msg); - } - - /** - * Returns a stub object that, when connected, will listen for marshaled - * {@link IDownloaderClient} methods and translate them into calls to the supplied - * interface. - * - * @param itf An implementation of IDownloaderClient that will be called - * when remote method calls are unmarshaled. - * @param downloaderService The class for your implementation of {@link - * impl.DownloaderService}. - * @return The {@link IStub} that allows you to connect to the service such that - * your {@link IDownloaderClient} receives status updates. - */ - public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { - return new Stub(itf, downloaderService); - } - - /** - * Starts the download if necessary. This function starts a flow that does ` - * many things. 1) Checks to see if the APK version has been checked and - * the metadata database updated 2) If the APK version does not match, - * checks the new LVL status to see if a new download is required 3) If the - * APK version does match, then checks to see if the download(s) have been - * completed 4) If the downloads have been completed, returns - * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the - * startup of an application to quickly ascertain if the application needs - * to wait to hear about any updated APK expansion files. Note that this does - * mean that the application MUST be run for the first time with a network - * connection, even if Market delivers all of the files. - * - * @param context Your application Context. - * @param notificationClient A PendingIntent to start the Activity in your application - * that shows the download progress and which will also start the application when download - * completes. - * @param serviceClass the class of your {@link imp.DownloaderService} implementation - * @return whether the service was started and the reason for starting the service. - * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link - * #DOWNLOAD_REQUIRED}. - * @throws NameNotFoundException - */ - public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, - Class<?> serviceClass) - throws NameNotFoundException { - return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, - serviceClass); - } - - /** - * This version assumes that the intent contains the pending intent as a parameter. This - * is used for responding to alarms. - * <p>The pending intent must be in an extra with the key {@link - * impl.DownloaderService#EXTRA_PENDING_INTENT}. - * - * @param context - * @param notificationClient - * @param serviceClass the class of the service to start - * @return - * @throws NameNotFoundException - */ - public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, - Class<?> serviceClass) - throws NameNotFoundException { - return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, - serviceClass); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java deleted file mode 100644 index f75debe32d..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +++ /dev/null @@ -1,193 +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; - -import com.google.android.vending.expansion.downloader.impl.DownloaderService; - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; - -import java.lang.ref.WeakReference; - -/** - * This class is used by the client activity to proxy requests to the Downloader - * Service. - * - * Most importantly, you must call {@link #CreateProxy} during the {@link - * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate - * an {@link IDownloaderService} object that you can then use to issue commands to the {@link - * DownloaderService} (such as to pause and resume downloads). - */ -public class DownloaderServiceMarshaller { - - public static final int MSG_REQUEST_ABORT_DOWNLOAD = - 1; - public static final int MSG_REQUEST_PAUSE_DOWNLOAD = - 2; - public static final int MSG_SET_DOWNLOAD_FLAGS = - 3; - public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = - 4; - public static final int MSG_REQUEST_DOWNLOAD_STATE = - 5; - public static final int MSG_REQUEST_CLIENT_UPDATE = - 6; - - public static final String PARAMS_FLAGS = "flags"; - public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; - - private static class Proxy implements IDownloaderService { - private Messenger mMsg; - - private void send(int method, Bundle params) { - Message m = Message.obtain(null, method); - m.setData(params); - try { - mMsg.send(m); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - public Proxy(Messenger msg) { - mMsg = msg; - } - - @Override - public void requestAbortDownload() { - send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); - } - - @Override - public void requestPauseDownload() { - send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); - } - - @Override - public void setDownloadFlags(int flags) { - Bundle params = new Bundle(); - params.putInt(PARAMS_FLAGS, flags); - send(MSG_SET_DOWNLOAD_FLAGS, params); - } - - @Override - public void requestContinueDownload() { - send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); - } - - @Override - public void requestDownloadStatus() { - send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); - } - - @Override - public void onClientUpdated(Messenger clientMessenger) { - Bundle bundle = new Bundle(1); - bundle.putParcelable(PARAM_MESSENGER, clientMessenger); - send(MSG_REQUEST_CLIENT_UPDATE, bundle); - } - } - - private static class Stub implements IStub { - private IDownloaderService mItf = null; - private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); - final Messenger mMessenger = new Messenger(mMsgHandler); - - private static class MessengerHandlerServer extends Handler { - private final WeakReference<Stub> mDownloader; - public MessengerHandlerServer(Stub downloader) { - mDownloader = new WeakReference<>(downloader); - } - - @Override - public void handleMessage(Message msg) { - Stub downloader = mDownloader.get(); - if (downloader != null) { - downloader.handleMessage(msg); - } - } - } - - private void handleMessage(Message msg) { - switch (msg.what) { - case MSG_REQUEST_ABORT_DOWNLOAD: - mItf.requestAbortDownload(); - break; - case MSG_REQUEST_CONTINUE_DOWNLOAD: - mItf.requestContinueDownload(); - break; - case MSG_REQUEST_PAUSE_DOWNLOAD: - mItf.requestPauseDownload(); - break; - case MSG_SET_DOWNLOAD_FLAGS: - mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); - break; - case MSG_REQUEST_DOWNLOAD_STATE: - mItf.requestDownloadStatus(); - break; - case MSG_REQUEST_CLIENT_UPDATE: - mItf.onClientUpdated((Messenger)msg.getData().getParcelable( - PARAM_MESSENGER)); - break; - } - } - - public Stub(IDownloaderService itf) { - mItf = itf; - } - - @Override - public Messenger getMessenger() { - return mMessenger; - } - - @Override - public void connect(Context c) { - } - - @Override - public void disconnect(Context c) { - } - } - - /** - * Returns a proxy that will marshall calls to IDownloaderService methods - * - * @param ctx - * @return - */ - public static IDownloaderService CreateProxy(Messenger msg) { - return new Proxy(msg); - } - - /** - * Returns a stub object that, when connected, will listen for marshalled - * IDownloaderService methods and translate them into calls to the supplied - * interface. - * - * @param itf An implementation of IDownloaderService that will be called - * when remote method calls are unmarshalled. - * @return - */ - public static IStub CreateStub(IDownloaderService itf) { - return new Stub(itf); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java deleted file mode 100644 index cd8726533f..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +++ /dev/null @@ -1,355 +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; - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.os.Environment; -import android.os.StatFs; -import android.os.SystemClock; -import android.util.Log; - -import com.godot.game.R; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Random; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Some helper functions for the download manager - */ -public class Helpers { - - public static Random sRandom = new Random(SystemClock.uptimeMillis()); - - /** Regex used to parse content-disposition headers */ - private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern - .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); - - private Helpers() { - } - - /* - * Parse the Content-Disposition HTTP Header. The format of the header is defined here: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for - * content that is going to be downloaded to the file system. We only support the attachment - * type. - */ - static String parseContentDisposition(String contentDisposition) { - try { - Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); - if (m.find()) { - return m.group(1); - } - } catch (IllegalStateException ex) { - // This function is defined as returning null when it can't parse - // the header - } - return null; - } - - /** - * @return the root of the filesystem containing the given path - */ - public static File getFilesystemRoot(String path) { - File cache = Environment.getDownloadCacheDirectory(); - if (path.startsWith(cache.getPath())) { - return cache; - } - File external = Environment.getExternalStorageDirectory(); - if (path.startsWith(external.getPath())) { - return external; - } - throw new IllegalArgumentException( - "Cannot determine filesystem root for " + path); - } - - public static boolean isExternalMediaMounted() { - if (!Environment.getExternalStorageState().equals( - Environment.MEDIA_MOUNTED)) { - // No SD card found. - if (Constants.LOGVV) { - Log.d(Constants.TAG, "no external storage"); - } - return false; - } - return true; - } - - /** - * @return the number of bytes available on the filesystem rooted at the given File - */ - public static long getAvailableBytes(File root) { - StatFs stat = new StatFs(root.getPath()); - // put a bit of margin (in case creating the file grows the system by a - // few blocks) - long availableBlocks = (long)stat.getAvailableBlocks() - 4; - return stat.getBlockSize() * availableBlocks; - } - - /** - * Checks whether the filename looks legitimate - */ - public static boolean isFilenameValid(String filename) { - filename = filename.replaceFirst("/+", "/"); // normalize leading - // slashes - return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) || filename.startsWith(Environment.getExternalStorageDirectory().toString()); - } - - /* - * Delete the given file from device - */ - /* package */ static void deleteFile(String path) { - try { - File file = new File(path); - file.delete(); - } catch (Exception e) { - Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); - } - } - - /** - * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total - * file size, but given what we know about the expected ranges of file sizes for APK expansion - * files, it's probably not necessary. - * - * @param overallProgress - * @param overallTotal - * @return - */ - - static public String getDownloadProgressString(long overallProgress, long overallTotal) { - if (overallTotal == 0) { - if (Constants.LOGVV) { - Log.e(Constants.TAG, "Notification called when total is zero"); - } - return ""; - } - return String.format(Locale.ENGLISH, "%.2f", - (float)overallProgress / (1024.0f * 1024.0f)) + - "MB /" + - String.format(Locale.ENGLISH, "%.2f", (float)overallTotal / (1024.0f * 1024.0f)) + "MB"; - } - - /** - * Adds a percentile to getDownloadProgressString. - * - * @param overallProgress - * @param overallTotal - * @return - */ - static public String getDownloadProgressStringNotification(long overallProgress, - long overallTotal) { - if (overallTotal == 0) { - if (Constants.LOGVV) { - Log.e(Constants.TAG, "Notification called when total is zero"); - } - return ""; - } - return getDownloadProgressString(overallProgress, overallTotal) + " (" + - getDownloadProgressPercent(overallProgress, overallTotal) + ")"; - } - - public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { - if (overallTotal == 0) { - if (Constants.LOGVV) { - Log.e(Constants.TAG, "Notification called when total is zero"); - } - return ""; - } - return Long.toString(overallProgress * 100 / overallTotal) + "%"; - } - - public static String getSpeedString(float bytesPerMillisecond) { - return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); - } - - public static String getTimeRemaining(long durationInMilliseconds) { - SimpleDateFormat sdf; - if (durationInMilliseconds > 1000 * 60 * 60) { - sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); - } else { - sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); - } - return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); - } - - /** - * Returns the file name (without full path) for an Expansion APK file from the given context. - * - * @param c the context - * @param mainFile true for main file, false for patch file - * @param versionCode the version of the file - * @return String the file name of the expansion file - */ - public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { - return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; - } - - /** - * Returns the filename (where the file should be saved) from info about a download - */ - static public String generateSaveFileName(Context c, String fileName) { - String path = getSaveFilePath(c) + File.separator + fileName; - return path; - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - static public String getSaveFilePath(Context c) { - // This technically existed since Honeycomb, but it is critical - // on KitKat and greater versions since it will create the - // directory if needed - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return c.getObbDir().toString(); - } else { - File root = Environment.getExternalStorageDirectory(); - String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); - return path; - } - } - - /** - * Helper function to ascertain the existence of a file and return true/false appropriately - * - * @param c the app/activity/service context - * @param fileName the name (sans path) of the file to query - * @param fileSize the size that the file must match - * @param deleteFileOnMismatch if the file sizes do not match, delete the file - * @return true if it does exist, false otherwise - */ - static public boolean doesFileExist(Context c, String fileName, long fileSize, - boolean deleteFileOnMismatch) { - // the file may have been delivered by Play --- let's make sure - // it's the size we expect - File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); - if (fileForNewFile.exists()) { - if (fileForNewFile.length() == fileSize) { - return true; - } - if (deleteFileOnMismatch) { - // delete the file --- we won't be able to resume - // because we cannot confirm the integrity of the file - fileForNewFile.delete(); - } - } - return false; - } - - public static final int FS_READABLE = 0; - public static final int FS_DOES_NOT_EXIST = 1; - public static final int FS_CANNOT_READ = 2; - - /** - * Helper function to ascertain whether a file can be read. - * - * @param c the app/activity/service context - * @param fileName the name (sans path) of the file to query - * @return true if it does exist, false otherwise - */ - static public int getFileStatus(Context c, String fileName) { - // the file may have been delivered by Play --- let's make sure - // it's the size we expect - File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); - int returnValue; - if (fileForNewFile.exists()) { - if (fileForNewFile.canRead()) { - returnValue = FS_READABLE; - } else { - returnValue = FS_CANNOT_READ; - } - } else { - returnValue = FS_DOES_NOT_EXIST; - } - return returnValue; - } - - /** - * Helper function to ascertain whether the application has the correct access to the OBB - * directory to allow an OBB file to be written. - * - * @param c the app/activity/service context - * @return true if the application can write an OBB file, false otherwise - */ - static public boolean canWriteOBBFile(Context c) { - String path = getSaveFilePath(c); - File fileForNewFile = new File(path); - boolean canWrite; - if (fileForNewFile.exists()) { - canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite(); - } else { - canWrite = fileForNewFile.mkdirs(); - } - return canWrite; - } - - /** - * Converts download states that are returned by the - * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful - * if using the state strings built into the library to display user messages. - * - * @param state One of the STATE_* constants from {@link IDownloaderClient}. - * @return string resource ID for the corresponding string. - */ - static public int getDownloaderStringResourceIDFromState(int state) { - switch (state) { - case IDownloaderClient.STATE_IDLE: - return R.string.state_idle; - case IDownloaderClient.STATE_FETCHING_URL: - return R.string.state_fetching_url; - case IDownloaderClient.STATE_CONNECTING: - return R.string.state_connecting; - case IDownloaderClient.STATE_DOWNLOADING: - return R.string.state_downloading; - case IDownloaderClient.STATE_COMPLETED: - return R.string.state_completed; - case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: - return R.string.state_paused_network_unavailable; - case IDownloaderClient.STATE_PAUSED_BY_REQUEST: - return R.string.state_paused_by_request; - case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: - return R.string.state_paused_wifi_disabled; - case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: - return R.string.state_paused_wifi_unavailable; - case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: - return R.string.state_paused_wifi_disabled; - case IDownloaderClient.STATE_PAUSED_NEED_WIFI: - return R.string.state_paused_wifi_unavailable; - case IDownloaderClient.STATE_PAUSED_ROAMING: - return R.string.state_paused_roaming; - case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: - return R.string.state_paused_network_setup_failure; - case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: - return R.string.state_paused_sdcard_unavailable; - case IDownloaderClient.STATE_FAILED_UNLICENSED: - return R.string.state_failed_unlicensed; - case IDownloaderClient.STATE_FAILED_FETCHING_URL: - return R.string.state_failed_fetching_url; - case IDownloaderClient.STATE_FAILED_SDCARD_FULL: - return R.string.state_failed_sdcard_full; - case IDownloaderClient.STATE_FAILED_CANCELED: - return R.string.state_failed_cancelled; - default: - return R.string.state_unknown; - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java deleted file mode 100644 index c5577d4c2a..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +++ /dev/null @@ -1,126 +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; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.telephony.TelephonyManager; -import android.util.Log; - -/** - * Contains useful helper functions, typically tied to the application context. - */ -class SystemFacade { - private Context mContext; - private NotificationManager mNotificationManager; - - public SystemFacade(Context context) { - mContext = context; - mNotificationManager = (NotificationManager) - mContext.getSystemService(Context.NOTIFICATION_SERVICE); - } - - public long currentTimeMillis() { - return System.currentTimeMillis(); - } - - public Integer getActiveNetworkType() { - ConnectivityManager connectivity = - (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - Log.w(Constants.TAG, "couldn't get connectivity manager"); - return null; - } - - @SuppressLint("MissingPermission") - NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); - if (activeInfo == null) { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "network is not available"); - } - return null; - } - return activeInfo.getType(); - } - - public boolean isNetworkRoaming() { - ConnectivityManager connectivity = - (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - Log.w(Constants.TAG, "couldn't get connectivity manager"); - return false; - } - - @SuppressLint("MissingPermission") - NetworkInfo info = connectivity.getActiveNetworkInfo(); - boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); - TelephonyManager tm = (TelephonyManager)mContext - .getSystemService(Context.TELEPHONY_SERVICE); - if (null == tm) { - Log.w(Constants.TAG, "couldn't get telephony manager"); - return false; - } - boolean isRoaming = isMobile && tm.isNetworkRoaming(); - if (Constants.LOGVV && isRoaming) { - Log.v(Constants.TAG, "network is roaming"); - } - return isRoaming; - } - - public Long getMaxBytesOverMobile() { - return (long)Integer.MAX_VALUE; - } - - public Long getRecommendedMaxBytesOverMobile() { - return 2097152L; - } - - public void sendBroadcast(Intent intent) { - mContext.sendBroadcast(intent); - } - - public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { - return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; - } - - public void postNotification(long id, Notification notification) { - /** - * TODO: The system notification manager takes ints, not longs, as IDs, - * but the download manager uses IDs take straight from the database, - * which are longs. This will have to be dealt with at some point. - */ - mNotificationManager.notify((int)id, notification); - } - - public void cancelNotification(long id) { - mNotificationManager.cancel((int)id); - } - - public void cancelAllNotifications() { - mNotificationManager.cancelAll(); - } - - public void startThread(Thread thread) { - thread.start(); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java deleted file mode 100755 index 6346d7703a..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java +++ /dev/null @@ -1,111 +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 android.app.Service; -import android.content.Intent; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.util.Log; - -/** - * This service differs from IntentService in a few minor ways/ It will not - * auto-stop itself after the intent is handled unless the target returns "true" - * in should stop. Since the goal of this service is to handle a single kind of - * intent, it does not queue up batches of intents of the same type. - */ -public abstract class CustomIntentService extends Service { - private String mName; - private boolean mRedelivery; - private volatile ServiceHandler mServiceHandler; - private volatile Looper mServiceLooper; - private static final String LOG_TAG = "CustomIntentService"; - private static final int WHAT_MESSAGE = -10; - - public CustomIntentService(String paramString) { - this.mName = paramString; - } - - @Override - public IBinder onBind(Intent paramIntent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - HandlerThread localHandlerThread = new HandlerThread("IntentService[" + this.mName + "]"); - localHandlerThread.start(); - this.mServiceLooper = localHandlerThread.getLooper(); - this.mServiceHandler = new ServiceHandler(this.mServiceLooper); - } - - @Override - public void onDestroy() { - Thread localThread = this.mServiceLooper.getThread(); - if ((localThread != null) && (localThread.isAlive())) { - localThread.interrupt(); - } - this.mServiceLooper.quit(); - Log.d(LOG_TAG, "onDestroy"); - } - - protected abstract void onHandleIntent(Intent paramIntent); - - protected abstract boolean shouldStop(); - - @Override - public void onStart(Intent paramIntent, int startId) { - if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { - Message localMessage = this.mServiceHandler.obtainMessage(); - localMessage.arg1 = startId; - localMessage.obj = paramIntent; - localMessage.what = WHAT_MESSAGE; - this.mServiceHandler.sendMessage(localMessage); - } - } - - @Override - public int onStartCommand(Intent paramIntent, int flags, int startId) { - onStart(paramIntent, startId); - return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; - } - - public void setIntentRedelivery(boolean enabled) { - this.mRedelivery = enabled; - } - - private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message paramMessage) { - CustomIntentService.this - .onHandleIntent((Intent)paramMessage.obj); - if (shouldStop()) { - Log.d(LOG_TAG, "stopSelf"); - CustomIntentService.this.stopSelf(paramMessage.arg1); - Log.d(LOG_TAG, "afterStopSelf"); - } - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java deleted file mode 100644 index 0e72b7ae77..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java +++ /dev/null @@ -1,92 +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.google.android.vending.expansion.downloader.Constants; -import com.google.android.vending.expansion.downloader.Helpers; - -import android.util.Log; - -/** - * Representation of information about an individual download from the database. - */ -public class DownloadInfo { - public String mUri; - public final int mIndex; - public final String mFileName; - public String mETag; - public long mTotalBytes; - public long mCurrentBytes; - public long mLastMod; - public int mStatus; - public int mControl; - public int mNumFailed; - public int mRetryAfter; - public int mRedirectCount; - - boolean mInitialized; - - public int mFuzz; - - public DownloadInfo(int index, String fileName, String pkg) { - mFuzz = Helpers.sRandom.nextInt(1001); - mFileName = fileName; - mIndex = index; - } - - public void resetDownload() { - mCurrentBytes = 0; - mETag = ""; - mLastMod = 0; - mStatus = 0; - mControl = 0; - mNumFailed = 0; - mRetryAfter = 0; - mRedirectCount = 0; - } - - /** - * Returns the time when a download should be restarted. - */ - public long restartTime(long now) { - if (mNumFailed == 0) { - return now; - } - if (mRetryAfter > 0) { - return mLastMod + mRetryAfter; - } - return mLastMod + - Constants.RETRY_FIRST_DELAY * - (1000 + mFuzz) * (1 << (mNumFailed - 1)); - } - - public void logVerboseInfo() { - Log.v(Constants.TAG, "Service adding new entry"); - Log.v(Constants.TAG, "FILENAME: " + mFileName); - Log.v(Constants.TAG, "URI : " + mUri); - Log.v(Constants.TAG, "FILENAME: " + mFileName); - Log.v(Constants.TAG, "CONTROL : " + mControl); - Log.v(Constants.TAG, "STATUS : " + mStatus); - Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); - Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); - Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); - Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); - Log.v(Constants.TAG, "TOTAL : " + mTotalBytes); - Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); - Log.v(Constants.TAG, "ETAG : " + mETag); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java deleted file mode 100644 index 099e3f05b3..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +++ /dev/null @@ -1,224 +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.DownloadProgressInfo; -import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; -import com.google.android.vending.expansion.downloader.Helpers; -import com.google.android.vending.expansion.downloader.IDownloaderClient; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.os.Build; -import android.os.Messenger; -import android.support.v4.app.NotificationCompat; - -/** - * This class handles displaying the notification associated with the download - * queue going on in the download manager. It handles multiple status types; - * Some require user interaction and some do not. Some of the user interactions - * may be transient. (for example: the user is queried to continue the download - * on 3G when it started on WiFi, but then the phone locks onto WiFi again so - * the prompt automatically goes away) - * <p/> - * The application interface for the downloader also needs to understand and - * handle these transient states. - */ -public class DownloadNotification implements IDownloaderClient { - - private int mState; - private final Context mContext; - private final NotificationManager mNotificationManager; - private CharSequence mCurrentTitle; - - private IDownloaderClient mClientProxy; - private NotificationCompat.Builder mActiveDownloadBuilder; - private NotificationCompat.Builder mBuilder; - private NotificationCompat.Builder mCurrentBuilder; - private CharSequence mLabel; - private String mCurrentText; - private DownloadProgressInfo mProgressInfo; - private PendingIntent mContentIntent; - - static final String LOGTAG = "DownloadNotification"; - static final int NOTIFICATION_ID = LOGTAG.hashCode(); - - public PendingIntent getClientIntent() { - return mContentIntent; - } - - public void setClientIntent(PendingIntent clientIntent) { - this.mBuilder.setContentIntent(clientIntent); - this.mActiveDownloadBuilder.setContentIntent(clientIntent); - this.mContentIntent = clientIntent; - } - - public void resendState() { - if (null != mClientProxy) { - mClientProxy.onDownloadStateChanged(mState); - } - } - - @Override - public void onDownloadStateChanged(int newState) { - if (null != mClientProxy) { - mClientProxy.onDownloadStateChanged(newState); - } - if (newState != mState) { - mState = newState; - if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) { - return; - } - int stringDownloadID; - int iconResource; - boolean ongoingEvent; - - // get the new title string and paused text - switch (newState) { - case 0: - iconResource = android.R.drawable.stat_sys_warning; - stringDownloadID = R.string.state_unknown; - ongoingEvent = false; - break; - - case IDownloaderClient.STATE_DOWNLOADING: - iconResource = android.R.drawable.stat_sys_download; - stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); - ongoingEvent = true; - break; - - case IDownloaderClient.STATE_FETCHING_URL: - case IDownloaderClient.STATE_CONNECTING: - iconResource = android.R.drawable.stat_sys_download_done; - stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); - ongoingEvent = true; - break; - - case IDownloaderClient.STATE_COMPLETED: - case IDownloaderClient.STATE_PAUSED_BY_REQUEST: - iconResource = android.R.drawable.stat_sys_download_done; - stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); - ongoingEvent = false; - break; - - case IDownloaderClient.STATE_FAILED: - case IDownloaderClient.STATE_FAILED_CANCELED: - case IDownloaderClient.STATE_FAILED_FETCHING_URL: - case IDownloaderClient.STATE_FAILED_SDCARD_FULL: - case IDownloaderClient.STATE_FAILED_UNLICENSED: - iconResource = android.R.drawable.stat_sys_warning; - stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); - ongoingEvent = false; - break; - - default: - iconResource = android.R.drawable.stat_sys_warning; - stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); - ongoingEvent = true; - break; - } - - mCurrentText = mContext.getString(stringDownloadID); - mCurrentTitle = mLabel; - mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText); - mCurrentBuilder.setSmallIcon(iconResource); - mCurrentBuilder.setContentTitle(mCurrentTitle); - mCurrentBuilder.setContentText(mCurrentText); - if (ongoingEvent) { - mCurrentBuilder.setOngoing(true); - } else { - mCurrentBuilder.setOngoing(false); - mCurrentBuilder.setAutoCancel(true); - } - mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); - } - } - - @Override - public void onDownloadProgress(DownloadProgressInfo progress) { - mProgressInfo = progress; - if (null != mClientProxy) { - mClientProxy.onDownloadProgress(progress); - } - if (progress.mOverallTotal <= 0) { - // we just show the text - mBuilder.setTicker(mCurrentTitle); - mBuilder.setSmallIcon(android.R.drawable.stat_sys_download); - mBuilder.setContentTitle(mCurrentTitle); - mBuilder.setContentText(mCurrentText); - mCurrentBuilder = mBuilder; - } else { - mActiveDownloadBuilder.setProgress((int)progress.mOverallTotal, (int)progress.mOverallProgress, false); - mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); - mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download); - mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText); - mActiveDownloadBuilder.setContentTitle(mLabel); - mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification, - Helpers.getTimeRemaining(progress.mTimeRemaining))); - mCurrentBuilder = mActiveDownloadBuilder; - } - mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); - } - - /** - * Called in response to onClientUpdated. Creates a new proxy and notifies - * it of the current state. - * - * @param msg the client Messenger to notify - */ - public void setMessenger(Messenger msg) { - mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); - if (null != mProgressInfo) { - mClientProxy.onDownloadProgress(mProgressInfo); - } - if (mState != -1) { - mClientProxy.onDownloadStateChanged(mState); - } - } - - /** - * Constructor - * - * @param ctx The context to use to obtain access to the Notification - * Service - */ - DownloadNotification(Context ctx, CharSequence applicationLabel) { - mState = -1; - mContext = ctx; - mLabel = applicationLabel; - mNotificationManager = (NotificationManager) - mContext.getSystemService(Context.NOTIFICATION_SERVICE); - mActiveDownloadBuilder = new NotificationCompat.Builder(ctx); - mBuilder = new NotificationCompat.Builder(ctx); - - // Set Notification category and priorities to something that makes sense for a long - // lived background task. - mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); - - mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); - - mCurrentBuilder = mBuilder; - } - - @Override - public void onServiceConnected(Messenger m) { - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java deleted file mode 100644 index 2fa146408b..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +++ /dev/null @@ -1,833 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.google.android.vending.expansion.downloader.Constants; -import com.google.android.vending.expansion.downloader.Helpers; -import com.google.android.vending.expansion.downloader.IDownloaderClient; - -import android.content.Context; -import android.os.PowerManager; -import android.os.Process; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SyncFailedException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Locale; - -/** - * Runs an actual download - */ -public class DownloadThread { - - private Context mContext; - private DownloadInfo mInfo; - private DownloaderService mService; - private final DownloadsDB mDB; - private final DownloadNotification mNotification; - private String mUserAgent; - - public DownloadThread(DownloadInfo info, DownloaderService service, - DownloadNotification notification) { - mContext = service; - mInfo = info; - mService = service; - mNotification = notification; - mDB = DownloadsDB.getDB(service); - mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" + android.os.Build.ID + ")" + - service.getPackageName(); - } - - /** - * Returns the default user agent - */ - private String userAgent() { - return mUserAgent; - } - - /** - * State for the entire run() method. - */ - private static class State { - public String mFilename; - public FileOutputStream mStream; - public boolean mCountRetry = false; - public int mRetryAfter = 0; - public int mRedirectCount = 0; - public String mNewUri; - public boolean mGotData = false; - public String mRequestUri; - - public State(DownloadInfo info, DownloaderService service) { - mRedirectCount = info.mRedirectCount; - mRequestUri = info.mUri; - mFilename = service.generateTempSaveFileName(info.mFileName); - } - } - - /** - * State within executeDownload() - */ - private static class InnerState { - public int mBytesSoFar = 0; - public int mBytesThisSession = 0; - public String mHeaderETag; - public boolean mContinuingDownload = false; - public String mHeaderContentLength; - public String mHeaderContentDisposition; - public String mHeaderContentLocation; - public int mBytesNotified = 0; - public long mTimeLastNotification = 0; - } - - /** - * Raised from methods called by run() to indicate that the current request - * should be stopped immediately. Note the message passed to this exception - * will be logged and therefore must be guaranteed not to contain any PII, - * meaning it generally can't include any information about the request URI, - * headers, or destination filename. - */ - private class StopRequest extends Throwable { - - private static final long serialVersionUID = 6338592678988347973L; - public int mFinalStatus; - - public StopRequest(int finalStatus, String message) { - super(message); - mFinalStatus = finalStatus; - } - - public StopRequest(int finalStatus, String message, Throwable throwable) { - super(message, throwable); - mFinalStatus = finalStatus; - } - } - - /** - * Raised from methods called by executeDownload() to indicate that the - * download should be retried immediately. - */ - private class RetryDownload extends Throwable { - - private static final long serialVersionUID = 6196036036517540229L; - } - - /** - * Executes the download in a separate thread - */ - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - State state = new State(mInfo, mService); - PowerManager.WakeLock wakeLock = null; - int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; - - try { - PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); - wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); - - if (Constants.LOGV) { - Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); - Log.v(Constants.TAG, " at " + mInfo.mUri); - } - - boolean finished = false; - while (!finished) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); - Log.v(Constants.TAG, " at " + mInfo.mUri); - } - // Set or unset proxy, which may have changed since last GET - // request. - // setDefaultProxy() supports null as proxy parameter. - URL url = new URL(state.mRequestUri); - HttpURLConnection request = (HttpURLConnection)url.openConnection(); - request.setRequestProperty("User-Agent", userAgent()); - try { - executeDownload(state, request); - finished = true; - } catch (RetryDownload exc) { - // fall through - } finally { - request.disconnect(); - request = null; - } - } - - if (Constants.LOGV) { - Log.v(Constants.TAG, "download completed for " + mInfo.mFileName); - Log.v(Constants.TAG, " at " + mInfo.mUri); - } - finalizeDestinationFile(state); - finalStatus = DownloaderService.STATUS_SUCCESS; - } catch (StopRequest error) { - // remove the cause before printing, in case it contains PII - Log.w(Constants.TAG, - "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage()); - error.printStackTrace(); - finalStatus = error.mFinalStatus; - // fall through to finally block - } catch (Throwable ex) { // sometimes the socket code throws unchecked - // exceptions - Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex); - finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; - // falls through to the code that reports an error - } finally { - if (wakeLock != null) { - wakeLock.release(); - wakeLock = null; - } - cleanupDestination(state, finalStatus); - notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, - state.mRedirectCount, state.mGotData, state.mFilename); - } - } - - /** - * Fully execute a single download request - setup and send the request, - * handle the response, and transfer the data to the destination file. - */ - private void executeDownload(State state, HttpURLConnection request) - throws StopRequest, RetryDownload { - InnerState innerState = new InnerState(); - byte data[] = new byte[Constants.BUFFER_SIZE]; - - checkPausedOrCanceled(state); - - setupDestinationFile(state, innerState); - addRequestHeaders(innerState, request); - - // check just before sending the request to avoid using an invalid - // connection at all - checkConnectivity(state); - - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING); - int responseCode = sendRequest(state, request); - handleExceptionalStatus(state, innerState, request, responseCode); - - if (Constants.LOGV) { - Log.v(Constants.TAG, "received response for " + mInfo.mUri); - } - - processResponseHeaders(state, innerState, request); - InputStream entityStream = openResponseEntity(state, request); - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING); - transferData(state, innerState, data, entityStream); - } - - /** - * Check if current connectivity is valid for this request. - */ - private void checkConnectivity(State state) throws StopRequest { - switch (mService.getNetworkAvailabilityState(mDB)) { - case DownloaderService.NETWORK_OK: - return; - case DownloaderService.NETWORK_NO_CONNECTION: - throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, - "waiting for network to return"); - case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: - throw new StopRequest( - DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION, - "waiting for wifi or for download over cellular to be authorized"); - case DownloaderService.NETWORK_CANNOT_USE_ROAMING: - throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, - "roaming is not allowed"); - case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE: - throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi"); - } - } - - /** - * Transfer as much data as possible from the HTTP response to the - * destination file. - * - * @param data buffer to use to read data - * @param entityStream stream for reading the HTTP response entity - */ - private void transferData(State state, InnerState innerState, byte[] data, - InputStream entityStream) throws StopRequest { - for (;;) { - int bytesRead = readFromResponse(state, innerState, data, entityStream); - if (bytesRead == -1) { // success, end of stream already reached - handleEndOfStream(state, innerState); - return; - } - - state.mGotData = true; - writeDataToDestination(state, data, bytesRead); - innerState.mBytesSoFar += bytesRead; - innerState.mBytesThisSession += bytesRead; - reportProgress(state, innerState); - - checkPausedOrCanceled(state); - } - } - - /** - * Called after a successful completion to take any necessary action on the - * downloaded file. - */ - private void finalizeDestinationFile(State state) throws StopRequest { - syncDestination(state); - String tempFilename = state.mFilename; - String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName); - if (!state.mFilename.equals(finalFilename)) { - File startFile = new File(tempFilename); - File destFile = new File(finalFilename); - if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) { - if (!startFile.renameTo(destFile)) { - throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, - "unable to finalize destination file"); - } - } else { - throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, - "file delivered with incorrect size. probably due to network not browser configured"); - } - } - } - - /** - * Called just before the thread finishes, regardless of status, to take any - * necessary action on the downloaded file. - */ - private void cleanupDestination(State state, int finalStatus) { - closeDestination(state); - if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) { - new File(state.mFilename).delete(); - state.mFilename = null; - } - } - - /** - * Sync the destination file to storage. - */ - private void syncDestination(State state) { - FileOutputStream downloadedFileStream = null; - try { - downloadedFileStream = new FileOutputStream(state.mFilename, true); - downloadedFileStream.getFD().sync(); - } catch (FileNotFoundException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); - } catch (SyncFailedException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while syncing file: ", ex); - } finally { - if (downloadedFileStream != null) { - try { - downloadedFileStream.close(); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException while closing synced file: ", ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while closing file: ", ex); - } - } - } - } - - /** - * Close the destination output stream. - */ - private void closeDestination(State state) { - try { - // close the file - if (state.mStream != null) { - state.mStream.close(); - state.mStream = null; - } - } catch (IOException ex) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "exception when closing the file after download : " + ex); - } - // nothing can really be done if the file can't be closed - } - } - - /** - * Check if the download has been paused or canceled, stopping the request - * appropriately if it has been. - */ - private void checkPausedOrCanceled(State state) throws StopRequest { - if (mService.getControl() == DownloaderService.CONTROL_PAUSED) { - int status = mService.getStatus(); - switch (status) { - case DownloaderService.STATUS_PAUSED_BY_APP: - throw new StopRequest(mService.getStatus(), - "download paused"); - } - } - } - - /** - * Report download progress through the database if necessary. - */ - private void reportProgress(State state, InnerState innerState) { - long now = System.currentTimeMillis(); - if (innerState.mBytesSoFar - innerState.mBytesNotified > Constants.MIN_PROGRESS_STEP && now - innerState.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) { - // we store progress updates to the database here - mInfo.mCurrentBytes = innerState.mBytesSoFar; - mDB.updateDownloadCurrentBytes(mInfo); - - innerState.mBytesNotified = innerState.mBytesSoFar; - innerState.mTimeLastNotification = now; - - long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar; - - if (Constants.LOGVV) { - Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " + mInfo.mTotalBytes); - Log.v(Constants.TAG, " total " + totalBytesSoFar + " out of " + mService.mTotalLength); - } - - mService.notifyUpdateBytes(totalBytesSoFar); - } - } - - /** - * Write a data buffer to the destination file. - * - * @param data buffer containing the data to write - * @param bytesRead how many bytes to write from the buffer - */ - private void writeDataToDestination(State state, byte[] data, int bytesRead) - throws StopRequest { - for (;;) { - try { - if (state.mStream == null) { - state.mStream = new FileOutputStream(state.mFilename, true); - } - state.mStream.write(data, 0, bytesRead); - // we close after every write --- this may be too inefficient - closeDestination(state); - return; - } catch (IOException ex) { - if (!Helpers.isExternalMediaMounted()) { - throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR, - "external media not mounted while writing destination file"); - } - - long availableBytes = - Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename)); - if (availableBytes < bytesRead) { - throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR, - "insufficient space while writing destination file", ex); - } - throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, - "while writing destination file: " + ex.toString(), ex); - } - } - } - - /** - * Called when we've reached the end of the HTTP response stream, to update - * the database and check for consistency. - */ - private void handleEndOfStream(State state, InnerState innerState) throws StopRequest { - mInfo.mCurrentBytes = innerState.mBytesSoFar; - // this should always be set from the market - // if ( innerState.mHeaderContentLength == null ) { - // mInfo.mTotalBytes = innerState.mBytesSoFar; - // } - mDB.updateDownload(mInfo); - - boolean lengthMismatched = (innerState.mHeaderContentLength != null) && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength)); - if (lengthMismatched) { - if (cannotResume(innerState)) { - throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, - "mismatched content length"); - } else { - throw new StopRequest(getFinalStatusForHttpError(state), - "closed socket before end of file"); - } - } - } - - private boolean cannotResume(InnerState innerState) { - return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null; - } - - /** - * Read some data from the HTTP response stream, handling I/O errors. - * - * @param data buffer to use to read data - * @param entityStream stream for reading the HTTP response entity - * @return the number of bytes actually read or -1 if the end of the stream - * has been reached - */ - private int readFromResponse(State state, InnerState innerState, byte[] data, - InputStream entityStream) throws StopRequest { - try { - return entityStream.read(data); - } catch (IOException ex) { - logNetworkState(); - mInfo.mCurrentBytes = innerState.mBytesSoFar; - mDB.updateDownload(mInfo); - if (cannotResume(innerState)) { - String message = "while reading response: " + ex.toString() + ", can't resume interrupted download with no ETag"; - throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, - message, ex); - } else { - throw new StopRequest(getFinalStatusForHttpError(state), - "while reading response: " + ex.toString(), ex); - } - } - } - - /** - * Open a stream for the HTTP response entity, handling I/O errors. - * - * @return an InputStream to read the response entity - */ - private InputStream openResponseEntity(State state, HttpURLConnection response) - throws StopRequest { - try { - return response.getInputStream(); - } catch (IOException ex) { - logNetworkState(); - throw new StopRequest(getFinalStatusForHttpError(state), - "while getting entity: " + ex.toString(), ex); - } - } - - private void logNetworkState() { - if (Constants.LOGX) { - Log.i(Constants.TAG, - "Net " + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up" : "Down")); - } - } - - /** - * Read HTTP response headers and take appropriate action, including setting - * up the destination file and updating the database. - */ - private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response) - throws StopRequest { - if (innerState.mContinuingDownload) { - // ignore response headers on resume requests - return; - } - - readResponseHeaders(state, innerState, response); - - try { - state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes); - } catch (DownloaderService.GenerateSaveFileError exc) { - throw new StopRequest(exc.mStatus, exc.mMessage); - } - try { - state.mStream = new FileOutputStream(state.mFilename); - } catch (FileNotFoundException exc) { - // make sure the directory exists - File pathFile = new File(Helpers.getSaveFilePath(mService)); - try { - if (pathFile.mkdirs()) { - state.mStream = new FileOutputStream(state.mFilename); - } - } catch (Exception ex) { - throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, - "while opening destination file: " + exc.toString(), exc); - } - } - if (Constants.LOGV) { - Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); - } - - updateDatabaseFromHeaders(state, innerState); - // check connectivity again now that we know the total size - checkConnectivity(state); - } - - /** - * Update necessary database fields based on values of HTTP response headers - * that have been read. - */ - private void updateDatabaseFromHeaders(State state, InnerState innerState) { - mInfo.mETag = innerState.mHeaderETag; - mDB.updateDownload(mInfo); - } - - /** - * Read headers from the HTTP response and store them into local state. - */ - private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response) - throws StopRequest { - String value = response.getHeaderField("Content-Disposition"); - if (value != null) { - innerState.mHeaderContentDisposition = value; - } - value = response.getHeaderField("Content-Location"); - if (value != null) { - innerState.mHeaderContentLocation = value; - } - value = response.getHeaderField("ETag"); - if (value != null) { - innerState.mHeaderETag = value; - } - String headerTransferEncoding = null; - value = response.getHeaderField("Transfer-Encoding"); - if (value != null) { - headerTransferEncoding = value; - } - String headerContentType = null; - value = response.getHeaderField("Content-Type"); - if (value != null) { - headerContentType = value; - if (!headerContentType.equals("application/vnd.android.obb")) { - throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, - "file delivered with incorrect Mime type"); - } - } - - if (headerTransferEncoding == null) { - long contentLength = response.getContentLength(); - if (value != null) { - // this is always set from Market - if (contentLength != -1 && contentLength != mInfo.mTotalBytes) { - // we're most likely on a bad wifi connection -- we should - // probably - // also look at the mime type --- but the size mismatch is - // enough - // to tell us that something is wrong here - Log.e(Constants.TAG, "Incorrect file size delivered."); - } else { - innerState.mHeaderContentLength = Long.toString(contentLength); - } - } - } else { - // Ignore content-length with transfer-encoding - 2616 4.4 3 - if (Constants.LOGVV) { - Log.v(Constants.TAG, - "ignoring content-length because of xfer-encoding"); - } - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Content-Disposition: " + - innerState.mHeaderContentDisposition); - Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); - Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); - Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag); - Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); - } - - boolean noSizeInfo = innerState.mHeaderContentLength == null && (headerTransferEncoding == null || !headerTransferEncoding.equalsIgnoreCase("chunked")); - if (noSizeInfo) { - throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, - "can't know size of download, giving up"); - } - } - - /** - * Check the HTTP response status and handle anything unusual (e.g. not - * 200/206). - */ - private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode) - throws StopRequest, RetryDownload { - if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { - handleServiceUnavailable(state, connection); - } - int expectedStatus = innerState.mContinuingDownload ? 206 : DownloaderService.STATUS_SUCCESS; - if (responseCode != expectedStatus) { - handleOtherStatus(state, innerState, responseCode); - } else { - // no longer redirected - state.mRedirectCount = 0; - } - } - - /** - * Handle a status that we don't know how to deal with properly. - */ - private void handleOtherStatus(State state, InnerState innerState, int statusCode) - throws StopRequest { - int finalStatus; - if (DownloaderService.isStatusError(statusCode)) { - finalStatus = statusCode; - } else if (statusCode >= 300 && statusCode < 400) { - finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT; - } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) { - finalStatus = DownloaderService.STATUS_CANNOT_RESUME; - } else { - finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE; - } - throw new StopRequest(finalStatus, "http error " + statusCode); - } - - /** - * Add headers for this download to the HTTP request to allow for resume. - */ - private void addRequestHeaders(InnerState innerState, HttpURLConnection request) { - if (innerState.mContinuingDownload) { - if (innerState.mHeaderETag != null) { - request.setRequestProperty("If-Match", innerState.mHeaderETag); - } - request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-"); - } - } - - /** - * Handle a 503 Service Unavailable status by processing the Retry-After - * header. - */ - private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP response code 503"); - } - state.mCountRetry = true; - String retryAfterValue = connection.getHeaderField("Retry-After"); - if (retryAfterValue != null) { - try { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Retry-After :" + retryAfterValue); - } - state.mRetryAfter = Integer.parseInt(retryAfterValue); - if (state.mRetryAfter < 0) { - state.mRetryAfter = 0; - } else { - if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { - state.mRetryAfter = Constants.MIN_RETRY_AFTER; - } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { - state.mRetryAfter = Constants.MAX_RETRY_AFTER; - } - state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); - state.mRetryAfter *= 1000; - } - } catch (NumberFormatException ex) { - // ignored - retryAfter stays 0 in this case. - } - } - throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY, - "got 503 Service Unavailable, will retry later"); - } - - /** - * Send the request to the server, handling any I/O exceptions. - */ - private int sendRequest(State state, HttpURLConnection request) - throws StopRequest { - try { - return request.getResponseCode(); - } catch (IllegalArgumentException ex) { - throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, - "while trying to execute request: " + ex.toString(), ex); - } catch (IOException ex) { - logNetworkState(); - throw new StopRequest(getFinalStatusForHttpError(state), - "while trying to execute request: " + ex.toString(), ex); - } - } - - private int getFinalStatusForHttpError(State state) { - if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) { - return DownloaderService.STATUS_WAITING_FOR_NETWORK; - } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { - state.mCountRetry = true; - return DownloaderService.STATUS_WAITING_TO_RETRY; - } else { - Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed); - return DownloaderService.STATUS_HTTP_DATA_ERROR; - } - } - - /** - * Prepare the destination file to receive data. If the file already exists, - * we'll set up appropriately for resumption. - */ - private void setupDestinationFile(State state, InnerState innerState) - throws StopRequest { - if (state.mFilename != null) { // only true if we've already run a - // thread for this download - if (!Helpers.isFilenameValid(state.mFilename)) { - // this should never happen - throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, - "found invalid internal destination filename"); - } - // We're resuming a download that got interrupted - File f = new File(state.mFilename); - if (f.exists()) { - long fileLength = f.length(); - if (fileLength == 0) { - // The download hadn't actually started, we can restart from - // scratch - f.delete(); - state.mFilename = null; - } else if (mInfo.mETag == null) { - // This should've been caught upon failure - f.delete(); - throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, - "Trying to resume a download that can't be resumed"); - } else { - // All right, we'll be able to resume this download - try { - state.mStream = new FileOutputStream(state.mFilename, true); - } catch (FileNotFoundException exc) { - throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, - "while opening destination for resuming: " + exc.toString(), exc); - } - innerState.mBytesSoFar = (int)fileLength; - if (mInfo.mTotalBytes != -1) { - innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); - } - innerState.mHeaderETag = mInfo.mETag; - innerState.mContinuingDownload = true; - } - } - } - - if (state.mStream != null) { - closeDestination(state); - } - } - - /** - * Stores information about the completed download, and notifies the - * initiating application. - */ - private void notifyDownloadCompleted( - int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, - String filename) { - updateDownloadDatabase( - status, countRetry, retryAfter, redirectCount, gotData, filename); - if (DownloaderService.isStatusCompleted(status)) { - // TBD: send status update? - } - } - - private void updateDownloadDatabase( - int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, - String filename) { - mInfo.mStatus = status; - mInfo.mRetryAfter = retryAfter; - mInfo.mRedirectCount = redirectCount; - mInfo.mLastMod = System.currentTimeMillis(); - if (!countRetry) { - mInfo.mNumFailed = 0; - } else if (gotData) { - mInfo.mNumFailed = 1; - } else { - mInfo.mNumFailed++; - } - mDB.updateDownload(mInfo); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java deleted file mode 100644 index 25a561ccd4..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +++ /dev/null @@ -1,1320 +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.google.android.vending.expansion.downloader.Constants; -import com.google.android.vending.expansion.downloader.DownloadProgressInfo; -import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; -import com.google.android.vending.expansion.downloader.Helpers; -import com.google.android.vending.expansion.downloader.IDownloaderClient; -import com.google.android.vending.expansion.downloader.IDownloaderService; -import com.google.android.vending.expansion.downloader.IStub; -import com.google.android.vending.licensing.AESObfuscator; -import com.google.android.vending.licensing.APKExpansionPolicy; -import com.google.android.vending.licensing.LicenseChecker; -import com.google.android.vending.licensing.LicenseCheckerCallback; -import com.google.android.vending.licensing.Policy; - -import android.annotation.SuppressLint; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; -import android.os.Handler; -import android.os.IBinder; -import android.os.Messenger; -import android.os.SystemClock; -import android.provider.Settings.Secure; -import android.telephony.TelephonyManager; -import android.util.Log; - -import java.io.File; - -/** - * Performs the background downloads requested by applications that use the - * Downloads provider. This service does not run as a foreground task, so - * Android may kill it off at will, but it will try to restart itself if it can. - * Note that Android by default will kill off any process that has an open file - * handle on the shared (SD Card) partition if the partition is unmounted. - */ -public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { - - public DownloaderService() { - super("LVLDownloadService"); - } - - private static final String LOG_TAG = "LVLDL"; - - // the following NETWORK_* constants are used to indicates specific reasons - // for disallowing a - // download from using a network, since specific causes can require special - // handling - - /** - * The network is usable for the given download. - */ - public static final int NETWORK_OK = 1; - - /** - * There is no network connectivity. - */ - public static final int NETWORK_NO_CONNECTION = 2; - - /** - * The download exceeds the maximum size for this network. - */ - public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; - - /** - * The download exceeds the recommended maximum size for this network, the - * user must confirm for this download to proceed without WiFi. - */ - public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; - - /** - * The current connection is roaming, and the download can't proceed over a - * roaming connection. - */ - public static final int NETWORK_CANNOT_USE_ROAMING = 5; - - /** - * The app requesting the download specific that it can't use the current - * network connection. - */ - public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; - - /** - * For intents used to notify the user that a download exceeds a size - * threshold, if this extra is true, WiFi is required for this download - * size; otherwise, it is only recommended. - */ - public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; - public static final String EXTRA_FILE_NAME = "downloadId"; - - /** - * Used with DOWNLOAD_STATUS - */ - public static final String EXTRA_STATUS_STATE = "ESS"; - public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS"; - public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS"; - public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP"; - public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP"; - - public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged"; - - /** - * Broadcast intent action sent by the download manager when a download - * completes. - */ - public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE"; - - /** - * Broadcast intent action sent by the download manager when download status - * changes. - */ - public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS"; - - /* - * Lists the states that the download manager can set on a download to - * notify applications of the download progress. The codes follow the HTTP - * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not - * used by the download manager)<br> 4xx: client errors<br> 5xx: server - * errors - */ - - /** - * Returns whether the status is informational (i.e. 1xx). - */ - public static boolean isStatusInformational(int status) { - return (status >= 100 && status < 200); - } - - /** - * Returns whether the status is a success (i.e. 2xx). - */ - public static boolean isStatusSuccess(int status) { - return (status >= 200 && status < 300); - } - - /** - * Returns whether the status is an error (i.e. 4xx or 5xx). - */ - public static boolean isStatusError(int status) { - return (status >= 400 && status < 600); - } - - /** - * Returns whether the status is a client error (i.e. 4xx). - */ - public static boolean isStatusClientError(int status) { - return (status >= 400 && status < 500); - } - - /** - * Returns whether the status is a server error (i.e. 5xx). - */ - public static boolean isStatusServerError(int status) { - return (status >= 500 && status < 600); - } - - /** - * Returns whether the download has completed (either with success or - * error). - */ - public static boolean isStatusCompleted(int status) { - return (status >= 200 && status < 300) || (status >= 400 && status < 600); - } - - /** - * This download hasn't stated yet - */ - public static final int STATUS_PENDING = 190; - - /** - * This download has started - */ - public static final int STATUS_RUNNING = 192; - - /** - * This download has been paused by the owning app. - */ - public static final int STATUS_PAUSED_BY_APP = 193; - - /** - * This download encountered some network error and is waiting before - * retrying the request. - */ - public static final int STATUS_WAITING_TO_RETRY = 194; - - /** - * This download is waiting for network connectivity to proceed. - */ - public static final int STATUS_WAITING_FOR_NETWORK = 195; - - /** - * This download is waiting for a Wi-Fi connection to proceed or for - * permission to download over cellular. - */ - public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196; - - /** - * This download is waiting for a Wi-Fi connection to proceed. - */ - public static final int STATUS_QUEUED_FOR_WIFI = 197; - - /** - * This download has successfully completed. Warning: there might be other - * status values that indicate success in the future. Use isSucccess() to - * capture the entire category. - * - * @hide - */ - public static final int STATUS_SUCCESS = 200; - - /** - * The requested URL is no longer available - */ - public static final int STATUS_FORBIDDEN = 403; - - /** - * The file was delivered incorrectly - */ - public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487; - - /** - * The requested destination file already exists. - */ - public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; - - /** - * Some possibly transient error occurred, but we can't resume the download. - */ - public static final int STATUS_CANNOT_RESUME = 489; - - /** - * This download was canceled - * - * @hide - */ - public static final int STATUS_CANCELED = 490; - - /** - * This download has completed with an error. Warning: there will be other - * status values that indicate errors in the future. Use isStatusError() to - * capture the entire category. - */ - public static final int STATUS_UNKNOWN_ERROR = 491; - - /** - * This download couldn't be completed because of a storage issue. - * Typically, that's because the filesystem is missing or full. Use the more - * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and - * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. - * - * @hide - */ - public static final int STATUS_FILE_ERROR = 492; - - /** - * This download couldn't be completed because of an HTTP redirect response - * that the download manager couldn't handle. - * - * @hide - */ - public static final int STATUS_UNHANDLED_REDIRECT = 493; - - /** - * This download couldn't be completed because of an unspecified unhandled - * HTTP code. - * - * @hide - */ - public static final int STATUS_UNHANDLED_HTTP_CODE = 494; - - /** - * This download couldn't be completed because of an error receiving or - * processing data at the HTTP level. - * - * @hide - */ - public static final int STATUS_HTTP_DATA_ERROR = 495; - - /** - * This download couldn't be completed because of an HttpException while - * setting up the request. - * - * @hide - */ - public static final int STATUS_HTTP_EXCEPTION = 496; - - /** - * This download couldn't be completed because there were too many - * redirects. - * - * @hide - */ - public static final int STATUS_TOO_MANY_REDIRECTS = 497; - - /** - * This download couldn't be completed due to insufficient storage space. - * Typically, this is because the SD card is full. - * - * @hide - */ - public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; - - /** - * This download couldn't be completed because no external storage device - * was found. Typically, this is because the SD card is not mounted. - * - * @hide - */ - public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; - - /** - * This download is allowed to run. - * - * @hide - */ - public static final int CONTROL_RUN = 0; - - /** - * This download must pause at the first opportunity. - * - * @hide - */ - public static final int CONTROL_PAUSED = 1; - - /** - * This download is visible but only shows in the notifications while it's - * in progress. - * - * @hide - */ - public static final int VISIBILITY_VISIBLE = 0; - - /** - * This download is visible and shows in the notifications while in progress - * and after completion. - * - * @hide - */ - public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; - - /** - * This download doesn't show in the UI or in the notifications. - * - * @hide - */ - public static final int VISIBILITY_HIDDEN = 2; - - /** - * Bit flag for setAllowedNetworkTypes corresponding to - * {@link ConnectivityManager#TYPE_MOBILE}. - */ - public static final int NETWORK_MOBILE = 1 << 0; - - /** - * Bit flag for setAllowedNetworkTypes corresponding to - * {@link ConnectivityManager#TYPE_WIFI}. - */ - public static final int NETWORK_WIFI = 1 << 1; - - private final static String TEMP_EXT = ".tmp"; - - /** - * Service thread status - */ - private static boolean sIsRunning; - - @Override - public IBinder onBind(Intent paramIntent) { - Log.d(Constants.TAG, "Service Bound"); - return this.mServiceMessenger.getBinder(); - } - - /** - * Network state. - */ - private boolean mIsConnected; - private boolean mIsFailover; - private boolean mIsCellularConnection; - private boolean mIsRoaming; - private boolean mIsAtLeast3G; - private boolean mIsAtLeast4G; - private boolean mStateChanged; - - /** - * Download state - */ - private int mControl; - private int mStatus; - - public boolean isWiFi() { - return mIsConnected && !mIsCellularConnection; - } - - /** - * Bindings to important services - */ - private ConnectivityManager mConnectivityManager; - private WifiManager mWifiManager; - - /** - * Package we are downloading for (defaults to package of application) - */ - private PackageInfo mPackageInfo; - - /** - * Byte counts - */ - long mBytesSoFar; - long mTotalLength; - int mFileCount; - - /** - * Used for calculating time remaining and speed - */ - long mBytesAtSample; - long mMillisecondsAtSample; - float mAverageDownloadSpeed; - - /** - * Our binding to the network state broadcasts - */ - private BroadcastReceiver mConnReceiver; - final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); - final private Messenger mServiceMessenger = mServiceStub.getMessenger(); - private Messenger mClientMessenger; - private DownloadNotification mNotification; - private PendingIntent mPendingIntent; - private PendingIntent mAlarmIntent; - - /** - * Updates the network type based upon the type and subtype returned from - * the connectivity manager. Subtype is only used for cellular signals. - * - * @param type - * @param subType - */ - private void updateNetworkType(int type, int subType) { - switch (type) { - case ConnectivityManager.TYPE_WIFI: - case ConnectivityManager.TYPE_ETHERNET: - case ConnectivityManager.TYPE_BLUETOOTH: - mIsCellularConnection = false; - mIsAtLeast3G = false; - mIsAtLeast4G = false; - break; - case ConnectivityManager.TYPE_WIMAX: - mIsCellularConnection = true; - mIsAtLeast3G = true; - mIsAtLeast4G = true; - break; - case ConnectivityManager.TYPE_MOBILE: - mIsCellularConnection = true; - switch (subType) { - case TelephonyManager.NETWORK_TYPE_1xRTT: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_IDEN: - mIsAtLeast3G = false; - mIsAtLeast4G = false; - break; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_UMTS: - mIsAtLeast3G = true; - mIsAtLeast4G = false; - break; - case TelephonyManager.NETWORK_TYPE_LTE: // 4G - case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop - // with 4G - case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but - // marketed as - // 4G - mIsAtLeast3G = true; - mIsAtLeast4G = true; - break; - default: - mIsCellularConnection = false; - mIsAtLeast3G = false; - mIsAtLeast4G = false; - } - } - } - - private void updateNetworkState(NetworkInfo info) { - boolean isConnected = mIsConnected; - boolean isFailover = mIsFailover; - boolean isCellularConnection = mIsCellularConnection; - boolean isRoaming = mIsRoaming; - boolean isAtLeast3G = mIsAtLeast3G; - if (null != info) { - mIsRoaming = info.isRoaming(); - mIsFailover = info.isFailover(); - mIsConnected = info.isConnected(); - updateNetworkType(info.getType(), info.getSubtype()); - } else { - mIsRoaming = false; - mIsFailover = false; - mIsConnected = false; - updateNetworkType(-1, -1); - } - mStateChanged = (mStateChanged || isConnected != mIsConnected || isFailover != mIsFailover || isCellularConnection != mIsCellularConnection || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G); - if (Constants.LOGVV) { - if (mStateChanged) { - Log.v(LOG_TAG, "Network state changed: "); - Log.v(LOG_TAG, "Starting State: " + - (isConnected ? "Connected " : "Not Connected ") + - (isCellularConnection ? "Cellular " : "WiFi ") + - (isRoaming ? "Roaming " : "Local ") + - (isAtLeast3G ? "3G+ " : "<3G ")); - Log.v(LOG_TAG, "Ending State: " + - (mIsConnected ? "Connected " : "Not Connected ") + - (mIsCellularConnection ? "Cellular " : "WiFi ") + - (mIsRoaming ? "Roaming " : "Local ") + - (mIsAtLeast3G ? "3G+ " : "<3G ")); - - if (isServiceRunning()) { - if (mIsRoaming) { - mStatus = STATUS_WAITING_FOR_NETWORK; - mControl = CONTROL_PAUSED; - } else if (mIsCellularConnection) { - DownloadsDB db = DownloadsDB.getDB(this); - int flags = db.getFlags(); - if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { - mStatus = STATUS_QUEUED_FOR_WIFI; - mControl = CONTROL_PAUSED; - } - } - } - } - } - } - - /** - * Polls the network state, setting the flags appropriately. - */ - void pollNetworkState() { - if (null == mConnectivityManager) { - mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - } - if (null == mWifiManager) { - mWifiManager = (WifiManager)getApplicationContext().getSystemService(Context.WIFI_SERVICE); - } - if (mConnectivityManager == null) { - Log.w(Constants.TAG, - "couldn't get connectivity manager to poll network state"); - } else { - @SuppressLint("MissingPermission") - NetworkInfo activeInfo = mConnectivityManager - .getActiveNetworkInfo(); - updateNetworkState(activeInfo); - } - } - - public static final int NO_DOWNLOAD_REQUIRED = 0; - public static final int LVL_CHECK_REQUIRED = 1; - public static final int DOWNLOAD_REQUIRED = 2; - - public static final String EXTRA_PACKAGE_NAME = "EPN"; - public static final String EXTRA_PENDING_INTENT = "EPI"; - public static final String EXTRA_MESSAGE_HANDLER = "EMH"; - - /** - * Returns true if the LVL check is required - * - * @param db a downloads DB synchronized with the latest state - * @param pi the package info for the project - * @return returns true if the filenames need to be returned - */ - private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) { - // we need to update the LVL check and get a successful status to - // proceed - if (db.mVersionCode != pi.versionCode) { - return true; - } - return false; - } - - /** - * Careful! Only use this internally. - * - * @return whether we think the service is running - */ - private static synchronized boolean isServiceRunning() { - return sIsRunning; - } - - private static synchronized void setServiceRunning(boolean isRunning) { - sIsRunning = isRunning; - } - - public static int startDownloadServiceIfRequired(Context context, - Intent intent, Class<?> serviceClass) throws NameNotFoundException { - final PendingIntent pendingIntent = (PendingIntent)intent - .getParcelableExtra(EXTRA_PENDING_INTENT); - return startDownloadServiceIfRequired(context, pendingIntent, - serviceClass); - } - - public static int startDownloadServiceIfRequired(Context context, - PendingIntent pendingIntent, Class<?> serviceClass) - throws NameNotFoundException { - String packageName = context.getPackageName(); - String className = serviceClass.getName(); - - return startDownloadServiceIfRequired(context, pendingIntent, - packageName, className); - } - - /** - * Starts the download if necessary. This function starts a flow that does ` - * many things. 1) Checks to see if the APK version has been checked and the - * metadata database updated 2) If the APK version does not match, checks - * the new LVL status to see if a new download is required 3) If the APK - * version does match, then checks to see if the download(s) have been - * completed 4) If the downloads have been completed, returns - * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the - * startup of an application to quickly ascertain if the application needs - * to wait to hear about any updated APK expansion files. Note that this - * does mean that the application MUST be run for the first time with a - * network connection, even if Market delivers all of the files. - * - * @param context - * @param pendingIntent - * @return true if the app should wait for more guidance from the - * downloader, false if the app can continue - * @throws NameNotFoundException - */ - public static int startDownloadServiceIfRequired(Context context, - PendingIntent pendingIntent, String classPackage, String className) - throws NameNotFoundException { - // first: do we need to do an LVL update? - // we begin by getting our APK version from the package manager - final PackageInfo pi = context.getPackageManager().getPackageInfo( - context.getPackageName(), 0); - - int status = NO_DOWNLOAD_REQUIRED; - - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(context); - - // we need to update the LVL check and get a successful status to - // proceed - if (isLVLCheckRequired(db, pi)) { - status = LVL_CHECK_REQUIRED; - } - // we don't have to update LVL. do we still have a download to start? - if (db.mStatus == 0) { - DownloadInfo[] infos = db.getDownloads(); - if (null != infos) { - for (DownloadInfo info : infos) { - if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) { - status = DOWNLOAD_REQUIRED; - db.updateStatus(-1); - break; - } - } - } - } else { - status = DOWNLOAD_REQUIRED; - } - switch (status) { - case DOWNLOAD_REQUIRED: - case LVL_CHECK_REQUIRED: - Intent fileIntent = new Intent(); - fileIntent.setClassName(classPackage, className); - fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); - context.startService(fileIntent); - break; - } - return status; - } - - @Override - public void requestAbortDownload() { - mControl = CONTROL_PAUSED; - mStatus = STATUS_CANCELED; - } - - @Override - public void requestPauseDownload() { - mControl = CONTROL_PAUSED; - mStatus = STATUS_PAUSED_BY_APP; - } - - @Override - public void setDownloadFlags(int flags) { - DownloadsDB.getDB(this).updateFlags(flags); - } - - @Override - public void requestContinueDownload() { - if (mControl == CONTROL_PAUSED) { - mControl = CONTROL_RUN; - } - Intent fileIntent = new Intent(this, this.getClass()); - fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - this.startService(fileIntent); - } - - public abstract String getPublicKey(); - - public abstract byte[] getSALT(); - - public abstract String getAlarmReceiverClassName(); - - private class LVLRunnable implements Runnable { - LVLRunnable(Context context, PendingIntent intent) { - mContext = context; - mPendingIntent = intent; - } - - final Context mContext; - - @Override - public void run() { - setServiceRunning(true); - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL); - String deviceId = Secure.ANDROID_ID; - - final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, - new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); - - // reset our policy back to the start of the world to force a - // re-check - aep.resetPolicy(); - - // let's try and get the OBB file from LVL first - // Construct the LicenseChecker with a Policy. - final LicenseChecker checker = new LicenseChecker(mContext, aep, - getPublicKey() // Your public licensing key. - ); - checker.checkAccess(new LicenseCheckerCallback() { - @Override - public void allow(int reason) { - try { - int count = aep.getExpansionURLCount(); - DownloadsDB db = DownloadsDB.getDB(mContext); - int status = 0; - if (count != 0) { - for (int i = 0; i < count; i++) { - String currentFileName = aep - .getExpansionFileName(i); - if (null != currentFileName) { - DownloadInfo di = new DownloadInfo(i, - currentFileName, mContext.getPackageName()); - - long fileSize = aep.getExpansionFileSize(i); - if (handleFileUpdated(db, i, currentFileName, - fileSize)) { - status |= -1; - di.resetDownload(); - di.mUri = aep.getExpansionURL(i); - di.mTotalBytes = fileSize; - di.mStatus = status; - db.updateDownload(di); - } else { - // we need to read the download - // information - // from - // the database - DownloadInfo dbdi = db - .getDownloadInfoByFileName(di.mFileName); - if (null == dbdi) { - // the file exists already and is - // the - // correct size - // was delivered by Market or - // through - // another mechanism - Log.d(LOG_TAG, "file " + di.mFileName + " found. Not downloading."); - di.mStatus = STATUS_SUCCESS; - di.mTotalBytes = fileSize; - di.mCurrentBytes = fileSize; - di.mUri = aep.getExpansionURL(i); - db.updateDownload(di); - } else if (dbdi.mStatus != STATUS_SUCCESS) { - // we just update the URL - dbdi.mUri = aep.getExpansionURL(i); - db.updateDownload(dbdi); - status |= -1; - } - } - } - } - } - // first: do we need to do an LVL update? - // we begin by getting our APK version from the package - // manager - PackageInfo pi; - try { - pi = mContext.getPackageManager().getPackageInfo( - mContext.getPackageName(), 0); - db.updateMetadata(pi.versionCode, status); - Class<?> serviceClass = DownloaderService.this.getClass(); - switch (startDownloadServiceIfRequired(mContext, mPendingIntent, - serviceClass)) { - case NO_DOWNLOAD_REQUIRED: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); - break; - case LVL_CHECK_REQUIRED: - // DANGER WILL ROBINSON! - Log.e(LOG_TAG, "In LVL checking loop!"); - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); - throw new RuntimeException( - "Error with LVL checking and database integrity"); - case DOWNLOAD_REQUIRED: - // do nothing. the download will notify the - // application - // when things are done - break; - } - } catch (NameNotFoundException e1) { - e1.printStackTrace(); - throw new RuntimeException( - "Error with getting information from package name"); - } - } finally { - setServiceRunning(false); - } - } - - @Override - public void dontAllow(int reason) { - try { - switch (reason) { - case Policy.NOT_LICENSED: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); - break; - case Policy.RETRY: - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); - break; - } - } finally { - setServiceRunning(false); - } - } - - @Override - public void applicationError(int errorCode) { - try { - mNotification - .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); - } finally { - setServiceRunning(false); - } - } - }); - } - }; - - /** - * Updates the LVL information from the server. - * - * @param context - */ - public void updateLVL(final Context context) { - Context c = context.getApplicationContext(); - Handler h = new Handler(c.getMainLooper()); - h.post(new LVLRunnable(c, mPendingIntent)); - } - - /** - * The APK has been updated and a filename has been sent down from the - * Market call. If the file has the same name as the previous file, we do - * nothing as the file is guaranteed to be the same. If the file does not - * have the same name, we download it if it hasn't already been delivered by - * Market. - * - * @param index the index of the file from market (0 = main, 1 = patch) - * @param filename the name of the new file - * @param fileSize the size of the new file - * @return - */ - public boolean handleFileUpdated(DownloadsDB db, int index, - String filename, long fileSize) { - DownloadInfo di = db.getDownloadInfoByFileName(filename); - if (null != di) { - String oldFile = di.mFileName; - // cleanup - if (null != oldFile) { - if (filename.equals(oldFile)) { - return false; - } - - // remove partially downloaded file if it is there - String deleteFile = Helpers.generateSaveFileName(this, oldFile); - File f = new File(deleteFile); - if (f.exists()) - f.delete(); - } - } - return !Helpers.doesFileExist(this, filename, fileSize, true); - } - - private void scheduleAlarm(long wakeUp) { - AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - if (alarms == null) { - Log.e(Constants.TAG, "couldn't get alarm manager"); - return; - } - - if (Constants.LOGV) { - Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); - } - - String className = getAlarmReceiverClassName(); - Intent intent = new Intent(Constants.ACTION_RETRY); - intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - intent.setClassName(this.getPackageName(), - className); - mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, - PendingIntent.FLAG_ONE_SHOT); - alarms.set( - AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + wakeUp, mAlarmIntent); - } - - private void cancelAlarms() { - if (null != mAlarmIntent) { - AlarmManager alarms = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - if (alarms == null) { - Log.e(Constants.TAG, "couldn't get alarm manager"); - return; - } - alarms.cancel(mAlarmIntent); - mAlarmIntent = null; - } - } - - /** - * We use this to track network state, such as when WiFi, Cellular, etc. is - * enabled when downloads are paused or in progress. - */ - private class InnerBroadcastReceiver extends BroadcastReceiver { - final Service mService; - - InnerBroadcastReceiver(Service service) { - mService = service; - } - - @Override - public void onReceive(Context context, Intent intent) { - pollNetworkState(); - if (mStateChanged && !isServiceRunning()) { - Log.d(Constants.TAG, "InnerBroadcastReceiver Called"); - Intent fileIntent = new Intent(context, mService.getClass()); - fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); - // send a new intent to the service - context.startService(fileIntent); - } - } - }; - - /** - * This is the main thread for the Downloader. This thread is responsible - * for queuing up downloads and other goodness. - */ - @Override - protected void onHandleIntent(Intent intent) { - setServiceRunning(true); - try { - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(this); - final PendingIntent pendingIntent = (PendingIntent)intent - .getParcelableExtra(EXTRA_PENDING_INTENT); - - if (null != pendingIntent) { - mNotification.setClientIntent(pendingIntent); - mPendingIntent = pendingIntent; - } else if (null != mPendingIntent) { - mNotification.setClientIntent(mPendingIntent); - } else { - Log.e(LOG_TAG, "Downloader started in bad state without notification intent."); - return; - } - - // when the LVL check completes, a successful response will update - // the service - if (isLVLCheckRequired(db, mPackageInfo)) { - updateLVL(this); - return; - } - - // get each download - DownloadInfo[] infos = db.getDownloads(); - mBytesSoFar = 0; - mTotalLength = 0; - mFileCount = infos.length; - for (DownloadInfo info : infos) { - // We do an (simple) integrity check on each file, just to make - // sure - if (info.mStatus == STATUS_SUCCESS) { - // verify that the file matches the state - if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) { - info.mStatus = 0; - info.mCurrentBytes = 0; - } - } - // get aggregate data - mTotalLength += info.mTotalBytes; - mBytesSoFar += info.mCurrentBytes; - } - - // loop through all downloads and fetch them - pollNetworkState(); - if (null == mConnReceiver) { - - /** - * We use this to track network state, such as when WiFi, - * Cellular, etc. is enabled when downloads are paused or in - * progress. - */ - mConnReceiver = new InnerBroadcastReceiver(this); - IntentFilter intentFilter = new IntentFilter( - ConnectivityManager.CONNECTIVITY_ACTION); - intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); - registerReceiver(mConnReceiver, intentFilter); - } - - for (DownloadInfo info : infos) { - long startingCount = info.mCurrentBytes; - - if (info.mStatus != STATUS_SUCCESS) { - DownloadThread dt = new DownloadThread(info, this, mNotification); - cancelAlarms(); - scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); - dt.run(); - cancelAlarms(); - } - db.updateFromDb(info); - boolean setWakeWatchdog = false; - int notifyStatus; - switch (info.mStatus) { - case STATUS_FORBIDDEN: - // the URL is out of date - updateLVL(this); - return; - case STATUS_SUCCESS: - mBytesSoFar += info.mCurrentBytes - startingCount; - db.updateMetadata(mPackageInfo.versionCode, 0); - continue; - case STATUS_FILE_DELIVERED_INCORRECTLY: - // we may be on a network that is returning us a web - // page on redirect - notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE; - info.mCurrentBytes = 0; - db.updateDownload(info); - setWakeWatchdog = true; - break; - case STATUS_PAUSED_BY_APP: - notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST; - break; - case STATUS_WAITING_FOR_NETWORK: - case STATUS_WAITING_TO_RETRY: - notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE; - setWakeWatchdog = true; - break; - case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION: - case STATUS_QUEUED_FOR_WIFI: - // look for more detail here - if (null != mWifiManager) { - if (!mWifiManager.isWifiEnabled()) { - notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION; - setWakeWatchdog = true; - break; - } - } - notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION; - setWakeWatchdog = true; - break; - case STATUS_CANCELED: - notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED; - setWakeWatchdog = true; - break; - - case STATUS_INSUFFICIENT_SPACE_ERROR: - notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL; - setWakeWatchdog = true; - break; - - case STATUS_DEVICE_NOT_FOUND_ERROR: - notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE; - setWakeWatchdog = true; - break; - - default: - notifyStatus = IDownloaderClient.STATE_FAILED; - break; - } - if (setWakeWatchdog) { - scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); - } else { - cancelAlarms(); - } - // failure or pause state - mNotification.onDownloadStateChanged(notifyStatus); - return; - } - - // all downloads complete - mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); - } finally { - setServiceRunning(false); - } - } - - @Override - public void onDestroy() { - if (null != mConnReceiver) { - unregisterReceiver(mConnReceiver); - mConnReceiver = null; - } - mServiceStub.disconnect(this); - super.onDestroy(); - } - - public int getNetworkAvailabilityState(DownloadsDB db) { - if (mIsConnected) { - if (!mIsCellularConnection) - return NETWORK_OK; - int flags = db.mFlags; - if (mIsRoaming) - return NETWORK_CANNOT_USE_ROAMING; - if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { - return NETWORK_OK; - } else { - return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; - } - } - return NETWORK_NO_CONNECTION; - } - - @Override - public void onCreate() { - super.onCreate(); - try { - mPackageInfo = getPackageManager().getPackageInfo( - getPackageName(), 0); - ApplicationInfo ai = getApplicationInfo(); - CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai); - mNotification = new DownloadNotification(this, applicationLabel); - - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - } - - /** - * Exception thrown from methods called by generateSaveFile() for any fatal - * error. - */ - public static class GenerateSaveFileError extends Exception { - private static final long serialVersionUID = 3465966015408936540L; - int mStatus; - String mMessage; - - public GenerateSaveFileError(int status, String message) { - mStatus = status; - mMessage = message; - } - } - - /** - * Returns the filename (where the file should be saved) from info about a - * download - */ - public String generateTempSaveFileName(String fileName) { - String path = Helpers.getSaveFilePath(this) + File.separator + fileName + TEMP_EXT; - return path; - } - - /** - * Creates a filename (where the file should be saved) from info about a - * download. - */ - public String generateSaveFile(String filename, long filesize) - throws GenerateSaveFileError { - String path = generateTempSaveFileName(filename); - File expPath = new File(path); - if (!Helpers.isExternalMediaMounted()) { - Log.d(Constants.TAG, "External media not mounted: " + path); - throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR, - "external media is not yet mounted"); - } - if (expPath.exists()) { - Log.d(Constants.TAG, "File already exists: " + path); - throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR, - "requested destination file already exists"); - } - if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) { - throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR, - "insufficient space on external storage"); - } - return path; - } - - /** - * @return a non-localized string appropriate for logging corresponding to - * one of the NETWORK_* constants. - */ - public String getLogMessageForNetworkError(int networkError) { - switch (networkError) { - case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds recommended limit for mobile network"; - - case NETWORK_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds limit for mobile network"; - - case NETWORK_NO_CONNECTION: - return "no network connection available"; - - case NETWORK_CANNOT_USE_ROAMING: - return "download cannot use the current network connection because it is roaming"; - - case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: - return "download was requested to not use the current network type"; - - default: - return "unknown error with network connectivity"; - } - } - - public int getControl() { - return mControl; - } - - public int getStatus() { - return mStatus; - } - - /** - * Calculating a moving average for the speed so we don't get jumpy - * calculations for time etc. - */ - static private final float SMOOTHING_FACTOR = 0.005f; - - public void notifyUpdateBytes(long totalBytesSoFar) { - long timeRemaining; - long currentTime = SystemClock.uptimeMillis(); - if (0 != mMillisecondsAtSample) { - // we have a sample. - long timePassed = currentTime - mMillisecondsAtSample; - long bytesInSample = totalBytesSoFar - mBytesAtSample; - float currentSpeedSample = (float)bytesInSample / (float)timePassed; - if (0 != mAverageDownloadSpeed) { - mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed; - } else { - mAverageDownloadSpeed = currentSpeedSample; - } - timeRemaining = (long)((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed); - } else { - timeRemaining = -1; - } - mMillisecondsAtSample = currentTime; - mBytesAtSample = totalBytesSoFar; - mNotification.onDownloadProgress( - new DownloadProgressInfo(mTotalLength, - totalBytesSoFar, - timeRemaining, - mAverageDownloadSpeed)); - } - - @Override - protected boolean shouldStop() { - // the database automatically reads the metadata for version code - // and download status when the instance is created - DownloadsDB db = DownloadsDB.getDB(this); - if (db.mStatus == 0) { - return true; - } - return false; - } - - @Override - public void requestDownloadStatus() { - mNotification.resendState(); - } - - @Override - public void onClientUpdated(Messenger clientMessenger) { - this.mClientMessenger = clientMessenger; - mNotification.setMessenger(mClientMessenger); - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java deleted file mode 100755 index 5d8dce0bac..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java +++ /dev/null @@ -1,469 +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 android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDoneException; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteStatement; -import android.provider.BaseColumns; -import android.util.Log; - -public class DownloadsDB { - private static final String DATABASE_NAME = "DownloadsDB"; - private static final int DATABASE_VERSION = 7; - public static final String LOG_TAG = DownloadsDB.class.getName(); - final SQLiteOpenHelper mHelper; - SQLiteStatement mGetDownloadByIndex; - SQLiteStatement mUpdateCurrentBytes; - private static DownloadsDB mDownloadsDB; - long mMetadataRowID = -1; - int mVersionCode = -1; - int mStatus = -1; - int mFlags; - - static public synchronized DownloadsDB getDB(Context paramContext) { - if (null == mDownloadsDB) { - return new DownloadsDB(paramContext); - } - return mDownloadsDB; - } - - private SQLiteStatement getDownloadByIndexStatement() { - if (null == mGetDownloadByIndex) { - mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( - "SELECT " + BaseColumns._ID + " FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.INDEX + " = ?"); - } - return mGetDownloadByIndex; - } - - private SQLiteStatement getUpdateCurrentBytesStatement() { - if (null == mUpdateCurrentBytes) { - mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( - "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + " = ?" - + - " WHERE " + DownloadColumns.INDEX + " = ?"); - } - return mUpdateCurrentBytes; - } - - private DownloadsDB(Context paramContext) { - this.mHelper = new DownloadsContentDBHelper(paramContext); - final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); - // Query for the version code, the row ID of the metadata (for future - // updating) the status and the flags - Cursor cur = sqldb.rawQuery("SELECT " + - MetadataColumns.APKVERSION + "," + - BaseColumns._ID + "," + - MetadataColumns.DOWNLOAD_STATUS + "," + - MetadataColumns.FLAGS + - " FROM " + MetadataColumns.TABLE_NAME + " LIMIT 1", - null); - if (null != cur && cur.moveToFirst()) { - mVersionCode = cur.getInt(0); - mMetadataRowID = cur.getLong(1); - mStatus = cur.getInt(2); - mFlags = cur.getInt(3); - cur.close(); - } - mDownloadsDB = this; - } - - protected DownloadInfo getDownloadInfoByFileName(String fileName) { - final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); - Cursor itemcur = null; - try { - itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, - DownloadColumns.FILENAME + " = ?", - new String[] { - fileName }, - null, null, null); - if (null != itemcur && itemcur.moveToFirst()) { - return getDownloadInfoFromCursor(itemcur); - } - } finally { - if (null != itemcur) - itemcur.close(); - } - return null; - } - - public long getIDForDownloadInfo(final DownloadInfo di) { - return getIDByIndex(di.mIndex); - } - - public long getIDByIndex(int index) { - SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); - downloadByIndex.clearBindings(); - downloadByIndex.bindLong(1, index); - try { - return downloadByIndex.simpleQueryForLong(); - } catch (SQLiteDoneException e) { - return -1; - } - } - - public void updateDownloadCurrentBytes(final DownloadInfo di) { - SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); - downloadCurrentBytes.clearBindings(); - downloadCurrentBytes.bindLong(1, di.mCurrentBytes); - downloadCurrentBytes.bindLong(2, di.mIndex); - downloadCurrentBytes.execute(); - } - - public void close() { - this.mHelper.close(); - } - - protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { - DownloadsContentDBHelper(Context paramContext) { - super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); - } - - private String createTableQueryFromArray(String paramString, - String[][] paramArrayOfString) { - StringBuilder localStringBuilder = new StringBuilder(); - localStringBuilder.append("CREATE TABLE "); - localStringBuilder.append(paramString); - localStringBuilder.append(" ("); - int i = paramArrayOfString.length; - for (int j = 0;; j++) { - if (j >= i) { - localStringBuilder - .setLength(localStringBuilder.length() - 1); - localStringBuilder.append(");"); - return localStringBuilder.toString(); - } - String[] arrayOfString = paramArrayOfString[j]; - localStringBuilder.append(' '); - localStringBuilder.append(arrayOfString[0]); - localStringBuilder.append(' '); - localStringBuilder.append(arrayOfString[1]); - localStringBuilder.append(','); - } - } - - /** - * These two arrays must match and have the same order. For every Schema - * there must be a corresponding table name. - */ - static final private String[][][] sSchemas = { - DownloadColumns.SCHEMA, MetadataColumns.SCHEMA - }; - - static final private String[] sTables = { - DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME - }; - - /** - * Goes through all of the tables in sTables and drops each table if it - * exists. Altered to no longer make use of reflection. - */ - private void dropTables(SQLiteDatabase paramSQLiteDatabase) { - for (String table : sTables) { - try { - paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); - } catch (Exception localException) { - localException.printStackTrace(); - } - } - } - - /** - * Goes through all of the tables in sTables and creates a database with - * the corresponding schema described in sSchemas. Altered to no longer - * make use of reflection. - */ - public void onCreate(SQLiteDatabase paramSQLiteDatabase) { - int numSchemas = sSchemas.length; - for (int i = 0; i < numSchemas; i++) { - try { - String[][] schema = (String[][])sSchemas[i]; - paramSQLiteDatabase.execSQL(createTableQueryFromArray( - sTables[i], schema)); - } catch (Exception localException) { - while (true) - localException.printStackTrace(); - } - } - } - - public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, - int paramInt1, int paramInt2) { - Log.w(DownloadsContentDBHelper.class.getName(), - "Upgrading database from version " + paramInt1 + " to " + paramInt2 + ", which will destroy all old data"); - dropTables(paramSQLiteDatabase); - onCreate(paramSQLiteDatabase); - } - } - - public static class MetadataColumns implements BaseColumns { - public static final String APKVERSION = "APKVERSION"; - public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; - public static final String FLAGS = "DOWNLOADFLAGS"; - - public static final String[][] SCHEMA = { - { BaseColumns._ID, "INTEGER PRIMARY KEY" }, - { APKVERSION, "INTEGER" }, { DOWNLOAD_STATUS, "INTEGER" }, - { FLAGS, "INTEGER" } - }; - public static final String TABLE_NAME = "MetadataColumns"; - public static final String _ID = "MetadataColumns._id"; - } - - public static class DownloadColumns implements BaseColumns { - public static final String INDEX = "FILEIDX"; - public static final String URI = "URI"; - public static final String FILENAME = "FN"; - public static final String ETAG = "ETAG"; - - public static final String TOTALBYTES = "TOTALBYTES"; - public static final String CURRENTBYTES = "CURRENTBYTES"; - public static final String LASTMOD = "LASTMOD"; - - public static final String STATUS = "STATUS"; - public static final String CONTROL = "CONTROL"; - public static final String NUM_FAILED = "FAILCOUNT"; - public static final String RETRY_AFTER = "RETRYAFTER"; - public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; - - public static final String[][] SCHEMA = { - { BaseColumns._ID, "INTEGER PRIMARY KEY" }, - { INDEX, "INTEGER UNIQUE" }, { URI, "TEXT" }, - { FILENAME, "TEXT UNIQUE" }, { ETAG, "TEXT" }, - { TOTALBYTES, "INTEGER" }, { CURRENTBYTES, "INTEGER" }, - { LASTMOD, "INTEGER" }, { STATUS, "INTEGER" }, - { CONTROL, "INTEGER" }, { NUM_FAILED, "INTEGER" }, - { RETRY_AFTER, "INTEGER" }, { REDIRECT_COUNT, "INTEGER" } - }; - public static final String TABLE_NAME = "DownloadColumns"; - public static final String _ID = "DownloadColumns._id"; - } - - private static final String[] DC_PROJECTION = { - DownloadColumns.FILENAME, - DownloadColumns.URI, DownloadColumns.ETAG, - DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, - DownloadColumns.LASTMOD, DownloadColumns.STATUS, - DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, - DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, - DownloadColumns.INDEX - }; - - private static final int FILENAME_IDX = 0; - private static final int URI_IDX = 1; - private static final int ETAG_IDX = 2; - private static final int TOTALBYTES_IDX = 3; - private static final int CURRENTBYTES_IDX = 4; - private static final int LASTMOD_IDX = 5; - private static final int STATUS_IDX = 6; - private static final int CONTROL_IDX = 7; - private static final int NUM_FAILED_IDX = 8; - private static final int RETRY_AFTER_IDX = 9; - private static final int REDIRECT_COUNT_IDX = 10; - private static final int INDEX_IDX = 11; - - /** - * This function will add a new file to the database if it does not exist. - * - * @param di DownloadInfo that we wish to store - * @return the row id of the record to be updated/inserted, or -1 - */ - public boolean updateDownload(DownloadInfo di) { - ContentValues cv = new ContentValues(); - cv.put(DownloadColumns.INDEX, di.mIndex); - cv.put(DownloadColumns.FILENAME, di.mFileName); - cv.put(DownloadColumns.URI, di.mUri); - cv.put(DownloadColumns.ETAG, di.mETag); - cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); - cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); - cv.put(DownloadColumns.LASTMOD, di.mLastMod); - cv.put(DownloadColumns.STATUS, di.mStatus); - cv.put(DownloadColumns.CONTROL, di.mControl); - cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); - cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); - cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); - return updateDownload(di, cv); - } - - public boolean updateDownload(DownloadInfo di, ContentValues cv) { - long id = di == null ? -1 : getIDForDownloadInfo(di); - try { - final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); - if (id != -1) { - if (1 != sqldb.update(DownloadColumns.TABLE_NAME, - cv, DownloadColumns._ID + " = " + id, null)) { - return false; - } - } else { - return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, - DownloadColumns.URI, cv); - } - } catch (android.database.sqlite.SQLiteException ex) { - ex.printStackTrace(); - } - return false; - } - - public int getLastCheckedVersionCode() { - return mVersionCode; - } - - public boolean isDownloadRequired() { - final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); - Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.STATUS + " <> 0", null); - try { - if (null != cur && cur.moveToFirst()) { - return 0 == cur.getInt(0); - } - } finally { - if (null != cur) - cur.close(); - } - return true; - } - - public int getFlags() { - return mFlags; - } - - public boolean updateFlags(int flags) { - if (mFlags != flags) { - ContentValues cv = new ContentValues(); - cv.put(MetadataColumns.FLAGS, flags); - if (updateMetadata(cv)) { - mFlags = flags; - return true; - } else { - return false; - } - } else { - return true; - } - }; - - public boolean updateStatus(int status) { - if (mStatus != status) { - ContentValues cv = new ContentValues(); - cv.put(MetadataColumns.DOWNLOAD_STATUS, status); - if (updateMetadata(cv)) { - mStatus = status; - return true; - } else { - return false; - } - } else { - return true; - } - }; - - public boolean updateMetadata(ContentValues cv) { - final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); - if (-1 == this.mMetadataRowID) { - long newID = sqldb.insert(MetadataColumns.TABLE_NAME, - MetadataColumns.APKVERSION, cv); - if (-1 == newID) - return false; - mMetadataRowID = newID; - } else { - if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, - BaseColumns._ID + " = " + mMetadataRowID, null)) - return false; - } - return true; - } - - public boolean updateMetadata(int apkVersion, int downloadStatus) { - ContentValues cv = new ContentValues(); - cv.put(MetadataColumns.APKVERSION, apkVersion); - cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); - if (updateMetadata(cv)) { - mVersionCode = apkVersion; - mStatus = downloadStatus; - return true; - } else { - return false; - } - }; - - public boolean updateFromDb(DownloadInfo di) { - final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); - Cursor cur = null; - try { - cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, - DownloadColumns.FILENAME + "= ?", - new String[] { - di.mFileName }, - null, null, null); - if (null != cur && cur.moveToFirst()) { - setDownloadInfoFromCursor(di, cur); - return true; - } - return false; - } finally { - if (null != cur) { - cur.close(); - } - } - } - - public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { - di.mUri = cur.getString(URI_IDX); - di.mETag = cur.getString(ETAG_IDX); - di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); - di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); - di.mLastMod = cur.getLong(LASTMOD_IDX); - di.mStatus = cur.getInt(STATUS_IDX); - di.mControl = cur.getInt(CONTROL_IDX); - di.mNumFailed = cur.getInt(NUM_FAILED_IDX); - di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); - di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); - } - - public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { - DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), - cur.getString(FILENAME_IDX), this.getClass().getPackage().getName()); - setDownloadInfoFromCursor(di, cur); - return di; - } - - public DownloadInfo[] getDownloads() { - final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); - Cursor cur = null; - try { - cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, - null, null, null, null); - if (null != cur && cur.moveToFirst()) { - DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; - int idx = 0; - do { - DownloadInfo di = getDownloadInfoFromCursor(cur); - retInfos[idx++] = di; - } while (cur.moveToNext()); - return retInfos; - } - return null; - } finally { - if (null != cur) { - cur.close(); - } - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java deleted file mode 100644 index 02bd1f27f6..0000000000 --- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java +++ /dev/null @@ -1,191 +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 android.text.format.Time; - -import java.util.Calendar; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Helper for parsing an HTTP date. - */ -public final class HttpDateTime { - - /* - * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT - * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850, - * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format - * with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon - * YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS - * GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon - * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first - * digit is zero. Mon can be the full name of the month. - */ - private static final String HTTP_DATE_RFC_REGEXP = - "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" - + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; - - private static final String HTTP_DATE_ANSIC_REGEXP = - "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" - + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; - - /** - * The compiled version of the HTTP-date regular expressions. - */ - private static final Pattern HTTP_DATE_RFC_PATTERN = - Pattern.compile(HTTP_DATE_RFC_REGEXP); - private static final Pattern HTTP_DATE_ANSIC_PATTERN = - Pattern.compile(HTTP_DATE_ANSIC_REGEXP); - - private static class TimeOfDay { - TimeOfDay(int h, int m, int s) { - this.hour = h; - this.minute = m; - this.second = s; - } - - int hour; - int minute; - int second; - } - - public static long parse(String timeString) - throws IllegalArgumentException { - - int date = 1; - int month = Calendar.JANUARY; - int year = 1970; - TimeOfDay timeOfDay; - - Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); - if (rfcMatcher.find()) { - date = getDate(rfcMatcher.group(1)); - month = getMonth(rfcMatcher.group(2)); - year = getYear(rfcMatcher.group(3)); - timeOfDay = getTime(rfcMatcher.group(4)); - } else { - Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); - if (ansicMatcher.find()) { - month = getMonth(ansicMatcher.group(1)); - date = getDate(ansicMatcher.group(2)); - timeOfDay = getTime(ansicMatcher.group(3)); - year = getYear(ansicMatcher.group(4)); - } else { - throw new IllegalArgumentException(); - } - } - - // FIXME: Y2038 BUG! - if (year >= 2038) { - year = 2038; - month = Calendar.JANUARY; - date = 1; - } - - Time time = new Time(Time.TIMEZONE_UTC); - time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, - month, year); - return time.toMillis(false /* use isDst */); - } - - private static int getDate(String dateString) { - if (dateString.length() == 2) { - return (dateString.charAt(0) - '0') * 10 + (dateString.charAt(1) - '0'); - } else { - return (dateString.charAt(0) - '0'); - } - } - - /* - * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0 - * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20 - * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19 - * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9 - */ - private static int getMonth(String monthString) { - int hash = Character.toLowerCase(monthString.charAt(0)) + - Character.toLowerCase(monthString.charAt(1)) + - Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; - switch (hash) { - case 22: - return Calendar.JANUARY; - case 10: - return Calendar.FEBRUARY; - case 29: - return Calendar.MARCH; - case 32: - return Calendar.APRIL; - case 36: - return Calendar.MAY; - case 42: - return Calendar.JUNE; - case 40: - return Calendar.JULY; - case 26: - return Calendar.AUGUST; - case 37: - return Calendar.SEPTEMBER; - case 35: - return Calendar.OCTOBER; - case 48: - return Calendar.NOVEMBER; - case 9: - return Calendar.DECEMBER; - default: - throw new IllegalArgumentException(); - } - } - - private static int getYear(String yearString) { - if (yearString.length() == 2) { - int year = (yearString.charAt(0) - '0') * 10 + (yearString.charAt(1) - '0'); - if (year >= 70) { - return year + 1900; - } else { - return year + 2000; - } - } else if (yearString.length() == 3) { - // According to RFC 2822, three digit years should be added to 1900. - int year = (yearString.charAt(0) - '0') * 100 + (yearString.charAt(1) - '0') * 10 + (yearString.charAt(2) - '0'); - return year + 1900; - } else if (yearString.length() == 4) { - return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0'); - } else { - return 1970; - } - } - - private static TimeOfDay getTime(String timeString) { - // HH might be H - int i = 0; - int hour = timeString.charAt(i++) - '0'; - if (timeString.charAt(i) != ':') - hour = hour * 10 + (timeString.charAt(i++) - '0'); - // Skip ':' - i++; - - int minute = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0'); - // Skip ':' - i++; - - int second = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0'); - - return new TimeOfDay(hour, minute, second); - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java deleted file mode 100644 index feba3034c3..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/AESObfuscator.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.vending.licensing; - -import com.google.android.vending.licensing.util.Base64; -import com.google.android.vending.licensing.util.Base64DecoderException; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.security.spec.KeySpec; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -/** - * An Obfuscator that uses AES to encrypt data. - */ -public class AESObfuscator implements Obfuscator { - private static final String UTF8 = "UTF-8"; - private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; - private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; - private static final byte[] IV = { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; - private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; - - private Cipher mEncryptor; - private Cipher mDecryptor; - - /** - * @param salt an array of random bytes to use for each (un)obfuscation - * @param applicationId application identifier, e.g. the package name - * @param deviceId device identifier. Use as many sources as possible to - * create this unique identifier. - */ - public AESObfuscator(byte[] salt, String applicationId, String deviceId) { - try { - SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); - KeySpec keySpec = - new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); - SecretKey tmp = factory.generateSecret(keySpec); - SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); - mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); - mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); - mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); - mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); - } catch (GeneralSecurityException e) { - // This can't happen on a compatible Android device. - throw new RuntimeException("Invalid environment", e); - } - } - - public String obfuscate(String original, String key) { - if (original == null) { - return null; - } - try { - // Header is appended as an integrity check - return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Invalid environment", e); - } catch (GeneralSecurityException e) { - throw new RuntimeException("Invalid environment", e); - } - } - - public String unobfuscate(String obfuscated, String key) throws ValidationException { - if (obfuscated == null) { - return null; - } - try { - String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); - // Check for presence of header. This serves as a final integrity check, for cases - // where the block size is correct during decryption. - int headerIndex = result.indexOf(header + key); - if (headerIndex != 0) { - throw new ValidationException("Header not found (invalid data or key)" - + ":" + - obfuscated); - } - return result.substring(header.length() + key.length(), result.length()); - } catch (Base64DecoderException e) { - throw new ValidationException(e.getMessage() + ":" + obfuscated); - } catch (IllegalBlockSizeException e) { - throw new ValidationException(e.getMessage() + ":" + obfuscated); - } catch (BadPaddingException e) { - throw new ValidationException(e.getMessage() + ":" + obfuscated); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Invalid environment", e); - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java deleted file mode 100644 index 2c60e7e4b8..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/APKExpansionPolicy.java +++ /dev/null @@ -1,413 +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 android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import com.google.android.vending.licensing.util.URIQueryDecoder; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.Vector; - -/** - * Default policy. All policy decisions are based off of response data received - * from the licensing service. Specifically, the licensing server sends the - * following information: response validity period, error retry period, - * error retry count and a URL for restoring app access in unlicensed cases. - * <p> - * These values will vary based on the the way the application is configured in - * the Google Play publishing console, such as whether the application is - * marked as free or is within its refund period, as well as how often an - * application is checking with the licensing service. - * <p> - * Developers who need more fine grained control over their application's - * licensing policy should implement a custom Policy. - */ -public class APKExpansionPolicy implements Policy { - - private static final String TAG = "APKExpansionPolicy"; - private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy"; - private static final String PREF_LAST_RESPONSE = "lastResponse"; - private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; - private static final String PREF_RETRY_UNTIL = "retryUntil"; - private static final String PREF_MAX_RETRIES = "maxRetries"; - private static final String PREF_RETRY_COUNT = "retryCount"; - private static final String PREF_LICENSING_URL = "licensingUrl"; - private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; - private static final String DEFAULT_RETRY_UNTIL = "0"; - private static final String DEFAULT_MAX_RETRIES = "0"; - private static final String DEFAULT_RETRY_COUNT = "0"; - - private static final long MILLIS_PER_MINUTE = 60 * 1000; - - private long mValidityTimestamp; - private long mRetryUntil; - private long mMaxRetries; - private long mRetryCount; - private long mLastResponseTime = 0; - private int mLastResponse; - private String mLicensingUrl; - private PreferenceObfuscator mPreferences; - private Vector<String> mExpansionURLs = new Vector<String>(); - private Vector<String> mExpansionFileNames = new Vector<String>(); - private Vector<Long> mExpansionFileSizes = new Vector<Long>(); - - /** - * The design of the protocol supports n files. Currently the market can - * only deliver two files. To accommodate this, we have these two constants, - * but the order is the only relevant thing here. - */ - public static final int MAIN_FILE_URL_INDEX = 0; - public static final int PATCH_FILE_URL_INDEX = 1; - - /** - * @param context The context for the current application - * @param obfuscator An obfuscator to be used with preferences. - */ - public APKExpansionPolicy(Context context, Obfuscator obfuscator) { - // Import old values - SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); - mPreferences = new PreferenceObfuscator(sp, obfuscator); - mLastResponse = Integer.parseInt( - mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); - mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, - DEFAULT_VALIDITY_TIMESTAMP)); - mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); - mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); - mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); - mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); - } - - /** - * We call this to guarantee that we fetch a fresh policy from the server. - * This is to be used if the URL is invalid. - */ - public void resetPolicy() { - mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); - setRetryUntil(DEFAULT_RETRY_UNTIL); - setMaxRetries(DEFAULT_MAX_RETRIES); - setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); - setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); - mPreferences.commit(); - } - - /** - * Process a new response from the license server. - * <p> - * This data will be used for computing future policy decisions. The - * following parameters are processed: - * <ul> - * <li>VT: the timestamp that the client should consider the response valid - * until - * <li>GT: the timestamp that the client should ignore retry errors until - * <li>GR: the number of retry errors that the client should ignore - * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. - * buy app on the Play Store) - * </ul> - * - * @param response the result from validating the server response - * @param rawData the raw server response data - */ - public void processServerResponse(int response, - com.google.android.vending.licensing.ResponseData rawData) { - - // Update retry counter - if (response != Policy.RETRY) { - setRetryCount(0); - } else { - setRetryCount(mRetryCount + 1); - } - - // Update server policy data - Map<String, String> extras = decodeExtras(rawData); - if (response == Policy.LICENSED) { - mLastResponse = response; - // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. - setLicensingUrl(null); - setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); - Set<String> keys = extras.keySet(); - for (String key : keys) { - if (key.equals("VT")) { - setValidityTimestamp(extras.get(key)); - } else if (key.equals("GT")) { - setRetryUntil(extras.get(key)); - } else if (key.equals("GR")) { - setMaxRetries(extras.get(key)); - } else if (key.startsWith("FILE_URL")) { - int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; - setExpansionURL(index, extras.get(key)); - } else if (key.startsWith("FILE_NAME")) { - int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; - setExpansionFileName(index, extras.get(key)); - } else if (key.startsWith("FILE_SIZE")) { - int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; - setExpansionFileSize(index, Long.parseLong(extras.get(key))); - } - } - } else if (response == Policy.NOT_LICENSED) { - // Clear out stale retry params - setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); - setRetryUntil(DEFAULT_RETRY_UNTIL); - setMaxRetries(DEFAULT_MAX_RETRIES); - // Update the licensing URL - setLicensingUrl(extras.get("LU")); - } - - setLastResponse(response); - mPreferences.commit(); - } - - /** - * Set the last license response received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param l the response - */ - private void setLastResponse(int l) { - mLastResponseTime = System.currentTimeMillis(); - mLastResponse = l; - mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); - } - - /** - * Set the current retry count and add to preferences. You must manually - * call PreferenceObfuscator.commit() to commit these changes to disk. - * - * @param c the new retry count - */ - private void setRetryCount(long c) { - mRetryCount = c; - mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); - } - - public long getRetryCount() { - return mRetryCount; - } - - /** - * Set the last validity timestamp (VT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param validityTimestamp the VT string received - */ - private void setValidityTimestamp(String validityTimestamp) { - Long lValidityTimestamp; - try { - lValidityTimestamp = Long.parseLong(validityTimestamp); - } catch (NumberFormatException e) { - // No response or not parseable, expire in one minute. - Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); - lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; - validityTimestamp = Long.toString(lValidityTimestamp); - } - - mValidityTimestamp = lValidityTimestamp; - mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); - } - - public long getValidityTimestamp() { - return mValidityTimestamp; - } - - /** - * Set the retry until timestamp (GT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param retryUntil the GT string received - */ - private void setRetryUntil(String retryUntil) { - Long lRetryUntil; - try { - lRetryUntil = Long.parseLong(retryUntil); - } catch (NumberFormatException e) { - // No response or not parseable, expire immediately - Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); - retryUntil = "0"; - lRetryUntil = 0l; - } - - mRetryUntil = lRetryUntil; - mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); - } - - public long getRetryUntil() { - return mRetryUntil; - } - - /** - * Set the max retries value (GR) as received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param maxRetries the GR string received - */ - private void setMaxRetries(String maxRetries) { - Long lMaxRetries; - try { - lMaxRetries = Long.parseLong(maxRetries); - } catch (NumberFormatException e) { - // No response or not parseable, expire immediately - Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); - maxRetries = "0"; - lMaxRetries = 0l; - } - - mMaxRetries = lMaxRetries; - mPreferences.putString(PREF_MAX_RETRIES, maxRetries); - } - - public long getMaxRetries() { - return mMaxRetries; - } - - /** - * Set the licensing URL that displays a Play Store UI for the user to regain app access. - * - * @param url the LU string received - */ - private void setLicensingUrl(String url) { - mLicensingUrl = url; - mPreferences.putString(PREF_LICENSING_URL, url); - } - - public String getLicensingUrl() { - return mLicensingUrl; - } - - /** - * Gets the count of expansion URLs. Since expansionURLs are not committed - * to preferences, this will return zero if there has been no LVL fetch - * in the current session. - * - * @return the number of expansion URLs. (0,1,2) - */ - public int getExpansionURLCount() { - return mExpansionURLs.size(); - } - - /** - * Gets the expansion URL. Since these URLs are not committed to - * preferences, this will always return null if there has not been an LVL - * fetch in the current session. - * - * @param index the index of the URL to fetch. This value will be either - * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX - */ - public String getExpansionURL(int index) { - if (index < mExpansionURLs.size()) { - return mExpansionURLs.elementAt(index); - } - return null; - } - - /** - * Sets the expansion URL. Expansion URL's are not committed to preferences, - * but are instead intended to be stored when the license response is - * processed by the front-end. - * - * @param index the index of the expansion URL. This value will be either - * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX - * @param URL the URL to set - */ - public void setExpansionURL(int index, String URL) { - if (index >= mExpansionURLs.size()) { - mExpansionURLs.setSize(index + 1); - } - mExpansionURLs.set(index, URL); - } - - public String getExpansionFileName(int index) { - if (index < mExpansionFileNames.size()) { - return mExpansionFileNames.elementAt(index); - } - return null; - } - - public void setExpansionFileName(int index, String name) { - if (index >= mExpansionFileNames.size()) { - mExpansionFileNames.setSize(index + 1); - } - mExpansionFileNames.set(index, name); - } - - public long getExpansionFileSize(int index) { - if (index < mExpansionFileSizes.size()) { - return mExpansionFileSizes.elementAt(index); - } - return -1; - } - - public void setExpansionFileSize(int index, long size) { - if (index >= mExpansionFileSizes.size()) { - mExpansionFileSizes.setSize(index + 1); - } - mExpansionFileSizes.set(index, size); - } - - /** - * {@inheritDoc} This implementation allows access if either:<br> - * <ol> - * <li>a LICENSED response was received within the validity period - * <li>a RETRY response was received in the last minute, and we are under - * the RETRY count or in the RETRY period. - * </ol> - */ - public boolean allowAccess() { - long ts = System.currentTimeMillis(); - if (mLastResponse == Policy.LICENSED) { - // Check if the LICENSED response occurred within the validity - // timeout. - if (ts <= mValidityTimestamp) { - // Cached LICENSED response is still valid. - return true; - } - } else if (mLastResponse == Policy.RETRY && - ts < mLastResponseTime + MILLIS_PER_MINUTE) { - // Only allow access if we are within the retry period or we haven't - // used up our - // max retries. - return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); - } - return false; - } - - private Map<String, String> decodeExtras( - com.google.android.vending.licensing.ResponseData rawData) { - Map<String, String> results = new HashMap<String, String>(); - if (rawData == null) { - return results; - } - - try { - URI rawExtras = new URI("?" + rawData.extra); - URIQueryDecoder.DecodeQuery(rawExtras, results); - } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); - } - return results; - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java b/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java deleted file mode 100644 index 89edeae1b4..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/ILicenseResultListener.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicenseResultListener.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicenseResultListener extends android.os.IInterface { - /** Local-side IPC implementation stub class. */ - public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener { - private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; - /** Construct the stub at attach it to the interface. */ - public Stub() { - this.attachInterface(this, DESCRIPTOR); - } - /** - * Cast an IBinder object into an ILicenseResultListener interface, - * generating a proxy if needed. - */ - public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) { - if ((obj == null)) { - return null; - } - android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); - if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { - return ((com.google.android.vending.licensing.ILicenseResultListener)iin); - } - return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); - } - public android.os.IBinder asBinder() { - return this; - } - public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { - switch (code) { - case INTERFACE_TRANSACTION: { - reply.writeString(DESCRIPTOR); - return true; - } - case TRANSACTION_verifyLicense: { - data.enforceInterface(DESCRIPTOR); - int _arg0; - _arg0 = data.readInt(); - java.lang.String _arg1; - _arg1 = data.readString(); - java.lang.String _arg2; - _arg2 = data.readString(); - this.verifyLicense(_arg0, _arg1, _arg2); - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener { - private android.os.IBinder mRemote; - Proxy(android.os.IBinder remote) { - mRemote = remote; - } - public android.os.IBinder asBinder() { - return mRemote; - } - public java.lang.String getInterfaceDescriptor() { - return DESCRIPTOR; - } - public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeInt(responseCode); - _data.writeString(signedData); - _data.writeString(signature); - mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); - } finally { - _data.recycle(); - } - } - } - static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); - } - public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java b/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java deleted file mode 100644 index 8b7cc83541..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/ILicensingService.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicensingService.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicensingService extends android.os.IInterface { - /** Local-side IPC implementation stub class. */ - public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService { - private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; - /** Construct the stub at attach it to the interface. */ - public Stub() { - this.attachInterface(this, DESCRIPTOR); - } - /** - * Cast an IBinder object into an ILicensingService interface, - * generating a proxy if needed. - */ - public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) { - if ((obj == null)) { - return null; - } - android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); - if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicensingService))) { - return ((com.google.android.vending.licensing.ILicensingService)iin); - } - return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); - } - public android.os.IBinder asBinder() { - return this; - } - public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { - switch (code) { - case INTERFACE_TRANSACTION: { - reply.writeString(DESCRIPTOR); - return true; - } - case TRANSACTION_checkLicense: { - data.enforceInterface(DESCRIPTOR); - long _arg0; - _arg0 = data.readLong(); - java.lang.String _arg1; - _arg1 = data.readString(); - com.google.android.vending.licensing.ILicenseResultListener _arg2; - _arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); - this.checkLicense(_arg0, _arg1, _arg2); - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - private static class Proxy implements com.google.android.vending.licensing.ILicensingService { - private android.os.IBinder mRemote; - Proxy(android.os.IBinder remote) { - mRemote = remote; - } - public android.os.IBinder asBinder() { - return mRemote; - } - public java.lang.String getInterfaceDescriptor() { - return DESCRIPTOR; - } - public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException { - android.os.Parcel _data = android.os.Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - _data.writeLong(nonce); - _data.writeString(packageName); - _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null))); - mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); - } finally { - _data.recycle(); - } - } - } - static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); - } - public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java deleted file mode 100644 index 38aab9f4f5..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/LicenseChecker.java +++ /dev/null @@ -1,387 +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.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager.NameNotFoundException; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.RemoteException; -import android.provider.Settings.Secure; -import android.util.Log; - -import com.google.android.vending.licensing.ILicenseResultListener; -import com.google.android.vending.licensing.ILicensingService; -import com.google.android.vending.licensing.util.Base64; -import com.google.android.vending.licensing.util.Base64DecoderException; - -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; - -/** - * Client library for Google Play license verifications. - * <p> - * The LicenseChecker is configured via a {@link Policy} which contains the logic to determine - * whether a user should have access to the application. For example, the Policy can define a - * threshold for allowable number of server or client failures before the library reports the user - * as not having access. - * <p> - * Must also provide the Base64-encoded RSA public key associated with your developer account. The - * public key is obtainable from the publisher site. - */ -public class LicenseChecker implements ServiceConnection { - private static final String TAG = "LicenseChecker"; - - private static final String KEY_FACTORY_ALGORITHM = "RSA"; - - // Timeout value (in milliseconds) for calls to service. - private static final int TIMEOUT_MS = 10 * 1000; - - private static final SecureRandom RANDOM = new SecureRandom(); - private static final boolean DEBUG_LICENSE_ERROR = false; - - private ILicensingService mService; - - private PublicKey mPublicKey; - private final Context mContext; - private final Policy mPolicy; - /** - * A handler for running tasks on a background thread. We don't want license processing to block - * the UI thread. - */ - private Handler mHandler; - private final String mPackageName; - private final String mVersionCode; - private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>(); - private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>(); - - /** - * @param context a Context - * @param policy implementation of Policy - * @param encodedPublicKey Base64-encoded RSA public key - * @throws IllegalArgumentException if encodedPublicKey is invalid - */ - public LicenseChecker(Context context, Policy policy, String encodedPublicKey) { - mContext = context; - mPolicy = policy; - mPublicKey = generatePublicKey(encodedPublicKey); - mPackageName = mContext.getPackageName(); - mVersionCode = getVersionCode(context, mPackageName); - HandlerThread handlerThread = new HandlerThread("background thread"); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper()); - } - - /** - * Generates a PublicKey instance from a string containing the Base64-encoded public key. - * - * @param encodedPublicKey Base64-encoded public key - * @throws IllegalArgumentException if encodedPublicKey is invalid - */ - private static PublicKey generatePublicKey(String encodedPublicKey) { - try { - byte[] decodedKey = Base64.decode(encodedPublicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); - - return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); - } catch (NoSuchAlgorithmException e) { - // This won't happen in an Android-compatible environment. - throw new RuntimeException(e); - } catch (Base64DecoderException e) { - Log.e(TAG, "Could not decode from Base64."); - throw new IllegalArgumentException(e); - } catch (InvalidKeySpecException e) { - Log.e(TAG, "Invalid key specification."); - throw new IllegalArgumentException(e); - } - } - - /** - * Checks if the user should have access to the app. Binds the service if necessary. - * <p> - * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we - * recommend obfuscating the string that is passed into bindService using another method of your - * own devising. - * <p> - * source string: "com.android.vending.licensing.ILicensingService" - * <p> - * - * @param callback - */ - public synchronized void checkAccess(LicenseCheckerCallback callback) { - // If we have a valid recent LICENSED response, we can skip asking - // Market. - if (mPolicy.allowAccess()) { - Log.i(TAG, "Using cached license response"); - callback.allow(Policy.LICENSED); - } else { - LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(), - callback, generateNonce(), mPackageName, mVersionCode); - - if (mService == null) { - Log.i(TAG, "Binding to licensing service."); - try { - boolean bindResult = mContext - .bindService( - new Intent( - new String( - // Base64 encoded - - // com.android.vending.licensing.ILicensingService - // Consider encoding this in another way in your - // code to improve security - Base64.decode( - "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))) - // As of Android 5.0, implicit - // Service Intents are no longer - // allowed because it's not - // possible for the user to - // participate in disambiguating - // them. This does mean we break - // compatibility with Android - // Cupcake devices with this - // release, since setPackage was - // added in Donut. - .setPackage( - new String( - // Base64 - // encoded - - // com.android.vending - Base64.decode( - "Y29tLmFuZHJvaWQudmVuZGluZw=="))), - this, // ServiceConnection. - Context.BIND_AUTO_CREATE); - if (bindResult) { - mPendingChecks.offer(validator); - } else { - Log.e(TAG, "Could not bind to service."); - handleServiceConnectionError(validator); - } - } catch (SecurityException e) { - callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION); - } catch (Base64DecoderException e) { - e.printStackTrace(); - } - } else { - mPendingChecks.offer(validator); - runChecks(); - } - } - } - - /** - * Triggers the last deep link licensing URL returned from the server, which redirects users to a - * page which enables them to gain access to the app. If no such URL is returned by the server, it - * will go to the details page of the app in the Play Store. - */ - public void followLastLicensingUrl(Context context) { - String licensingUrl = mPolicy.getLicensingUrl(); - if (licensingUrl == null) { - licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName(); - } - Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl)); - context.startActivity(marketIntent); - } - - private void runChecks() { - LicenseValidator validator; - while ((validator = mPendingChecks.poll()) != null) { - try { - Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); - mService.checkLicense( - validator.getNonce(), validator.getPackageName(), - new ResultListener(validator)); - mChecksInProgress.add(validator); - } catch (RemoteException e) { - Log.w(TAG, "RemoteException in checkLicense call.", e); - handleServiceConnectionError(validator); - } - } - } - - private synchronized void finishCheck(LicenseValidator validator) { - mChecksInProgress.remove(validator); - if (mChecksInProgress.isEmpty()) { - cleanupService(); - } - } - - private class ResultListener extends ILicenseResultListener.Stub { - private final LicenseValidator mValidator; - private Runnable mOnTimeout; - - public ResultListener(LicenseValidator validator) { - mValidator = validator; - mOnTimeout = new Runnable() { - public void run() { - Log.i(TAG, "Check timed out."); - handleServiceConnectionError(mValidator); - finishCheck(mValidator); - } - }; - startTimeout(); - } - - private static final int ERROR_CONTACTING_SERVER = 0x101; - private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; - private static final int ERROR_NON_MATCHING_UID = 0x103; - - // Runs in IPC thread pool. Post it to the Handler, so we can guarantee - // either this or the timeout runs. - public void verifyLicense(final int responseCode, final String signedData, - final String signature) { - mHandler.post(new Runnable() { - public void run() { - Log.i(TAG, "Received response."); - // Make sure it hasn't already timed out. - if (mChecksInProgress.contains(mValidator)) { - clearTimeout(); - mValidator.verify(mPublicKey, responseCode, signedData, signature); - finishCheck(mValidator); - } - if (DEBUG_LICENSE_ERROR) { - boolean logResponse; - String stringError = null; - switch (responseCode) { - case ERROR_CONTACTING_SERVER: - logResponse = true; - stringError = "ERROR_CONTACTING_SERVER"; - break; - case ERROR_INVALID_PACKAGE_NAME: - logResponse = true; - stringError = "ERROR_INVALID_PACKAGE_NAME"; - break; - case ERROR_NON_MATCHING_UID: - logResponse = true; - stringError = "ERROR_NON_MATCHING_UID"; - break; - default: - logResponse = false; - } - - if (logResponse) { - String android_id = Secure.ANDROID_ID; - Date date = new Date(); - Log.d(TAG, "Server Failure: " + stringError); - Log.d(TAG, "Android ID: " + android_id); - Log.d(TAG, "Time: " + date.toGMTString()); - } - } - } - }); - } - - private void startTimeout() { - Log.i(TAG, "Start monitoring timeout."); - mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); - } - - private void clearTimeout() { - Log.i(TAG, "Clearing timeout."); - mHandler.removeCallbacks(mOnTimeout); - } - } - - public synchronized void onServiceConnected(ComponentName name, IBinder service) { - mService = ILicensingService.Stub.asInterface(service); - runChecks(); - } - - public synchronized void onServiceDisconnected(ComponentName name) { - // Called when the connection with the service has been - // unexpectedly disconnected. That is, Market crashed. - // If there are any checks in progress, the timeouts will handle them. - Log.w(TAG, "Service unexpectedly disconnected."); - mService = null; - } - - /** - * Generates policy response for service connection errors, as a result of disconnections or - * timeouts. - */ - private synchronized void handleServiceConnectionError(LicenseValidator validator) { - mPolicy.processServerResponse(Policy.RETRY, null); - - if (mPolicy.allowAccess()) { - validator.getCallback().allow(Policy.RETRY); - } else { - validator.getCallback().dontAllow(Policy.RETRY); - } - } - - /** Unbinds service if necessary and removes reference to it. */ - private void cleanupService() { - if (mService != null) { - try { - mContext.unbindService(this); - } catch (IllegalArgumentException e) { - // Somehow we've already been unbound. This is a non-fatal - // error. - Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); - } - mService = null; - } - } - - /** - * Inform the library that the context is about to be destroyed, so that any open connections - * can be cleaned up. - * <p> - * Failure to call this method can result in a crash under certain circumstances, such as during - * screen rotation if an Activity requests the license check or when the user exits the - * application. - */ - public synchronized void onDestroy() { - cleanupService(); - mHandler.getLooper().quit(); - } - - /** Generates a nonce (number used once). */ - private int generateNonce() { - return RANDOM.nextInt(); - } - - /** - * Get version code for the application package name. - * - * @param context - * @param packageName application package name - * @return the version code or empty string if package not found - */ - private static String getVersionCode(Context context, String packageName) { - try { - return String.valueOf( - context.getPackageManager().getPackageInfo(packageName, 0).versionCode); - } catch (NameNotFoundException e) { - Log.e(TAG, "Package not found. could not get version code."); - return ""; - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java deleted file mode 100644 index 77f7dc7295..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/LicenseValidator.java +++ /dev/null @@ -1,232 +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 { - if (TextUtils.isEmpty(signedData)) { - Log.e(TAG, "Signature verification failed: signedData is empty. " - + - "(Device not signed-in to any Google accounts?)"); - handleInvalidResponse(); - return; - } - - Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); - sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - - if (!sig.verify(Base64.decode(signature))) { - Log.e(TAG, "Signature verification failed."); - handleInvalidResponse(); - return; - } - } catch (NoSuchAlgorithmException e) { - // This can't happen on an Android compatible device. - throw new RuntimeException(e); - } catch (InvalidKeyException e) { - handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); - return; - } catch (SignatureException e) { - throw new RuntimeException(e); - } catch (Base64DecoderException e) { - Log.e(TAG, "Could not Base64-decode signature."); - handleInvalidResponse(); - return; - } - - // Parse and validate response. - try { - data = ResponseData.parse(signedData); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Could not parse response."); - handleInvalidResponse(); - return; - } - - if (data.responseCode != responseCode) { - Log.e(TAG, "Response codes don't match."); - handleInvalidResponse(); - return; - } - - if (data.nonce != mNonce) { - Log.e(TAG, "Nonce doesn't match."); - handleInvalidResponse(); - return; - } - - if (!data.packageName.equals(mPackageName)) { - Log.e(TAG, "Package name doesn't match."); - handleInvalidResponse(); - return; - } - - if (!data.versionCode.equals(mVersionCode)) { - Log.e(TAG, "Version codes don't match."); - handleInvalidResponse(); - return; - } - - // Application-specific user identifier. - userId = data.userId; - if (TextUtils.isEmpty(userId)) { - Log.e(TAG, "User identifier is empty."); - handleInvalidResponse(); - return; - } - } - - switch (responseCode) { - case LICENSED: - case LICENSED_OLD_KEY: - int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); - handleResponse(limiterResponse, data); - break; - case NOT_LICENSED: - handleResponse(Policy.NOT_LICENSED, data); - break; - case ERROR_CONTACTING_SERVER: - Log.w(TAG, "Error contacting licensing server."); - handleResponse(Policy.RETRY, data); - break; - case ERROR_SERVER_FAILURE: - Log.w(TAG, "An error has occurred on the licensing server."); - handleResponse(Policy.RETRY, data); - break; - case ERROR_OVER_QUOTA: - Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); - handleResponse(Policy.RETRY, data); - break; - case ERROR_INVALID_PACKAGE_NAME: - handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); - break; - case ERROR_NON_MATCHING_UID: - handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); - break; - case ERROR_NOT_MARKET_MANAGED: - handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); - break; - default: - Log.e(TAG, "Unknown response code for license check."); - handleInvalidResponse(); - } - } - - /** - * Confers with policy and calls appropriate callback method. - * - * @param response - * @param rawData - */ - private void handleResponse(int response, ResponseData rawData) { - // Update policy data and increment retry counter (if needed) - mPolicy.processServerResponse(response, rawData); - - // Given everything we know, including cached data, ask the policy if we should grant - // access. - if (mPolicy.allowAccess()) { - mCallback.allow(response); - } else { - mCallback.dontAllow(response); - } - } - - private void handleApplicationError(int code) { - mCallback.applicationError(code); - } - - private void handleInvalidResponse() { - mCallback.dontAllow(Policy.NOT_LICENSED); - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java deleted file mode 100644 index 099bb1c48b..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java +++ /dev/null @@ -1,78 +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(); - mEditor.apply(); - } - String obfuscatedValue = mObfuscator.obfuscate(value, key); - mEditor.putString(key, obfuscatedValue); - } - - public String getString(String key, String defValue) { - String result; - String value = mPreferences.getString(key, null); - if (value != null) { - try { - result = mObfuscator.unobfuscate(value, key); - } catch (ValidationException e) { - // Unable to unobfuscate, data corrupt or tampered - Log.w(TAG, "Validation error while reading preference: " + key); - result = defValue; - } - } else { - // Preference not found - result = defValue; - } - return result; - } - - public void commit() { - if (mEditor != null) { - mEditor.commit(); - mEditor = null; - } - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java deleted file mode 100644 index 1c802f8e45..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/ResponseData.java +++ /dev/null @@ -1,80 +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.text.TextUtils; - -import java.util.regex.Pattern; - -/** - * ResponseData from licensing server. - */ -public class ResponseData { - - public int responseCode; - public int nonce; - public String packageName; - public String versionCode; - public String userId; - public long timestamp; - /** Response-specific data. */ - public String extra; - - /** - * Parses response string into ResponseData. - * - * @param responseData response data string - * @throws IllegalArgumentException upon parsing error - * @return ResponseData object - */ - public static ResponseData parse(String responseData) { - // Must parse out main response data and response-specific data. - int index = responseData.indexOf(':'); - String mainData, extraData; - if (-1 == index) { - mainData = responseData; - extraData = ""; - } else { - mainData = responseData.substring(0, index); - extraData = index >= responseData.length() ? "" : responseData.substring(index + 1); - } - - String[] fields = TextUtils.split(mainData, Pattern.quote("|")); - if (fields.length < 6) { - throw new IllegalArgumentException("Wrong number of fields."); - } - - ResponseData data = new ResponseData(); - data.extra = extraData; - data.responseCode = Integer.parseInt(fields[0]); - data.nonce = Integer.parseInt(fields[1]); - data.packageName = fields[2]; - data.versionCode = fields[3]; - // Application-specific user identifier. - data.userId = fields[4]; - data.timestamp = Long.parseLong(fields[5]); - - return data; - } - - @Override - public String toString() { - return TextUtils.join("|", new Object[] { - responseCode, nonce, packageName, versionCode, - userId, timestamp }); - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java deleted file mode 100644 index b9a50c1104..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/ServerManagedPolicy.java +++ /dev/null @@ -1,299 +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.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import com.google.android.vending.licensing.util.URIQueryDecoder; - -/** - * Default policy. All policy decisions are based off of response data received - * from the licensing service. Specifically, the licensing server sends the - * following information: response validity period, error retry period, - * error retry count and a URL for restoring app access in unlicensed cases. - * <p> - * These values will vary based on the the way the application is configured in - * the Google Play publishing console, such as whether the application is - * marked as free or is within its refund period, as well as how often an - * application is checking with the licensing service. - * <p> - * Developers who need more fine grained control over their application's - * licensing policy should implement a custom Policy. - */ -public class ServerManagedPolicy implements Policy { - - private static final String TAG = "ServerManagedPolicy"; - private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy"; - private static final String PREF_LAST_RESPONSE = "lastResponse"; - private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; - private static final String PREF_RETRY_UNTIL = "retryUntil"; - private static final String PREF_MAX_RETRIES = "maxRetries"; - private static final String PREF_RETRY_COUNT = "retryCount"; - private static final String PREF_LICENSING_URL = "licensingUrl"; - private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; - private static final String DEFAULT_RETRY_UNTIL = "0"; - private static final String DEFAULT_MAX_RETRIES = "0"; - private static final String DEFAULT_RETRY_COUNT = "0"; - - private static final long MILLIS_PER_MINUTE = 60 * 1000; - - private long mValidityTimestamp; - private long mRetryUntil; - private long mMaxRetries; - private long mRetryCount; - private long mLastResponseTime = 0; - private int mLastResponse; - private String mLicensingUrl; - private PreferenceObfuscator mPreferences; - - /** - * @param context The context for the current application - * @param obfuscator An obfuscator to be used with preferences. - */ - public ServerManagedPolicy(Context context, Obfuscator obfuscator) { - // Import old values - SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); - mPreferences = new PreferenceObfuscator(sp, obfuscator); - mLastResponse = Integer.parseInt( - mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); - mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, - DEFAULT_VALIDITY_TIMESTAMP)); - mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); - mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); - mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); - mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); - } - - /** - * Process a new response from the license server. - * <p> - * This data will be used for computing future policy decisions. The - * following parameters are processed: - * <ul> - * <li>VT: the timestamp that the client should consider the response valid - * until - * <li>GT: the timestamp that the client should ignore retry errors until - * <li>GR: the number of retry errors that the client should ignore - * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. - * buy app on the Play Store) - * </ul> - * - * @param response the result from validating the server response - * @param rawData the raw server response data - */ - public void processServerResponse(int response, ResponseData rawData) { - - // Update retry counter - if (response != Policy.RETRY) { - setRetryCount(0); - } else { - setRetryCount(mRetryCount + 1); - } - - // Update server policy data - Map<String, String> extras = decodeExtras(rawData); - if (response == Policy.LICENSED) { - mLastResponse = response; - // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. - setLicensingUrl(null); - setValidityTimestamp(extras.get("VT")); - setRetryUntil(extras.get("GT")); - setMaxRetries(extras.get("GR")); - } else if (response == Policy.NOT_LICENSED) { - // Clear out stale retry params - setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); - setRetryUntil(DEFAULT_RETRY_UNTIL); - setMaxRetries(DEFAULT_MAX_RETRIES); - // Update the licensing URL - setLicensingUrl(extras.get("LU")); - } - - setLastResponse(response); - mPreferences.commit(); - } - - /** - * Set the last license response received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param l the response - */ - private void setLastResponse(int l) { - mLastResponseTime = System.currentTimeMillis(); - mLastResponse = l; - mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); - } - - /** - * Set the current retry count and add to preferences. You must manually - * call PreferenceObfuscator.commit() to commit these changes to disk. - * - * @param c the new retry count - */ - private void setRetryCount(long c) { - mRetryCount = c; - mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); - } - - public long getRetryCount() { - return mRetryCount; - } - - /** - * Set the last validity timestamp (VT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param validityTimestamp the VT string received - */ - private void setValidityTimestamp(String validityTimestamp) { - Long lValidityTimestamp; - try { - lValidityTimestamp = Long.parseLong(validityTimestamp); - } catch (NumberFormatException e) { - // No response or not parsable, expire in one minute. - Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); - lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; - validityTimestamp = Long.toString(lValidityTimestamp); - } - - mValidityTimestamp = lValidityTimestamp; - mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); - } - - public long getValidityTimestamp() { - return mValidityTimestamp; - } - - /** - * Set the retry until timestamp (GT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param retryUntil the GT string received - */ - private void setRetryUntil(String retryUntil) { - Long lRetryUntil; - try { - lRetryUntil = Long.parseLong(retryUntil); - } catch (NumberFormatException e) { - // No response or not parsable, expire immediately - Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); - retryUntil = "0"; - lRetryUntil = 0l; - } - - mRetryUntil = lRetryUntil; - mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); - } - - public long getRetryUntil() { - return mRetryUntil; - } - - /** - * Set the max retries value (GR) as received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * - * @param maxRetries the GR string received - */ - private void setMaxRetries(String maxRetries) { - Long lMaxRetries; - try { - lMaxRetries = Long.parseLong(maxRetries); - } catch (NumberFormatException e) { - // No response or not parsable, expire immediately - Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); - maxRetries = "0"; - lMaxRetries = 0l; - } - - mMaxRetries = lMaxRetries; - mPreferences.putString(PREF_MAX_RETRIES, maxRetries); - } - - public long getMaxRetries() { - return mMaxRetries; - } - - /** - * Set the license URL value (LU) as received from the server and add to preferences. You must - * manually call PreferenceObfuscator.commit() to commit these changes to disk. - * - * @param url the LU string received - */ - private void setLicensingUrl(String url) { - mLicensingUrl = url; - mPreferences.putString(PREF_LICENSING_URL, url); - } - - public String getLicensingUrl() { - return mLicensingUrl; - } - - /** - * {@inheritDoc} - * - * This implementation allows access if either:<br> - * <ol> - * <li>a LICENSED response was received within the validity period - * <li>a RETRY response was received in the last minute, and we are under - * the RETRY count or in the RETRY period. - * </ol> - */ - public boolean allowAccess() { - long ts = System.currentTimeMillis(); - if (mLastResponse == Policy.LICENSED) { - // Check if the LICENSED response occurred within the validity timeout. - if (ts <= mValidityTimestamp) { - // Cached LICENSED response is still valid. - return true; - } - } else if (mLastResponse == Policy.RETRY && - ts < mLastResponseTime + MILLIS_PER_MINUTE) { - // Only allow access if we are within the retry period or we haven't used up our - // max retries. - return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); - } - return false; - } - - private Map<String, String> decodeExtras( - com.google.android.vending.licensing.ResponseData rawData) { - Map<String, String> results = new HashMap<String, String>(); - if (rawData == null) { - return results; - } - - try { - URI rawExtras = new URI("?" + rawData.extra); - URIQueryDecoder.DecodeQuery(rawExtras, results); - } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); - } - return results; - } -} diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java deleted file mode 100644 index bd711aadf5..0000000000 --- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java +++ /dev/null @@ -1,556 +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 - */ - -import com.godot.game.BuildConfig; - -/** - * Base64 converter class. This code is not a full-blown MIME encoder; - * it simply converts binary data to base64 data and back. - * - * <p>Note {@link CharBase64} is a GWT-compatible implementation of this - * class. - */ -public class Base64 { - /** Specify encoding (value is {@code true}). */ - public final static boolean ENCODE = true; - - /** Specify decoding (value is {@code false}). */ - public final static boolean DECODE = false; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte)'='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte)'\n'; - - /** - * The 64 valid Base64 values. - */ - private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', - (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', - (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', - (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', - (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', - (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', - (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', - (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', - (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', - (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', - (byte)'9', (byte)'+', (byte)'/' }; - - /** - * The 64 valid web safe Base64 values. - */ - private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', - (byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', - (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', - (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', - (byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', - (byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', - (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', - (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', - (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', - (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', - (byte)'9', (byte)'-', (byte)'_' }; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - /** The web safe decodabet */ - private final static byte[] WEBSAFE_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 - 62, // Dash '-' sign at decimal 45 - -9, -9, // Decimal 46-47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91-94 - 63, // Underscore '_' at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - // Indicates white space in encoding - private final static byte WHITE_SPACE_ENC = -5; - // Indicates equals sign in encoding - private final static byte EQUALS_SIGN_ENC = -1; - - /** Defeats instantiation. */ - private Base64() { - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Encodes up to three bytes of the array <var>source</var> - * and writes the resulting four Base64 bytes to <var>destination</var>. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * <var>srcOffset</var> and <var>destOffset</var>. - * This method does not check to make sure your arrays - * are large enough to accommodate <var>srcOffset</var> + 3 for - * the <var>source</var> array or <var>destOffset</var> + 4 for - * the <var>destination</var> array. - * The actual number of significant bytes in your array is - * given by <var>numSigBytes</var>. - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param alphabet is the encoding alphabet - * @return the <var>destination</var> array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index alphabet - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = - (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = alphabet[(inBuff)&0x3f]; - return destination; - case 2: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - case 1: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Encodes a byte array into Base64 notation. - * Equivalent to calling - * {@code encodeBytes(source, 0, source.length)} - * - * @param source The data to convert - * @since 1.4 - */ - public static String encode(byte[] source) { - return encode(source, 0, source.length, ALPHABET, true); - } - - /** - * Encodes a byte array into web safe Base64 notation. - * - * @param source The data to convert - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - */ - public static String encodeWebSafe(byte[] source, boolean doPadding) { - return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - * @since 1.4 - */ - public static String encode(byte[] source, int off, int len, byte[] alphabet, - boolean doPadding) { - byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); - int outLen = outBuff.length; - - // If doPadding is false, set length to truncate '=' - // padding characters - while (doPadding == false && outLen > 0) { - if (outBuff[outLen - 1] != '=') { - break; - } - outLen -= 1; - } - - return new String(outBuff, 0, outLen); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param maxLineLength maximum length of one line. - * @return the BASE64-encoded byte array - */ - public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, - int maxLineLength) { - int lenDiv3 = (len + 2) / 3; // ceil(len / 3) - int len43 = lenDiv3 * 4; - byte[] outBuff = new byte[len43 // Main 4:3 - + (len43 / maxLineLength)]; // New lines - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - - // The following block of code is the same as - // encode3to4( source, d + off, 3, outBuff, e, alphabet ); - // but inlined for faster encoding (~20% improvement) - int inBuff = - ((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24); - outBuff[e] = alphabet[(inBuff >>> 18)]; - outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - outBuff[e + 3] = alphabet[(inBuff)&0x3f]; - - lineLength += 4; - if (lineLength == maxLineLength) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // end for: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, alphabet); - - lineLength += 4; - if (lineLength == maxLineLength) { - // Add a last newline - outBuff[e + 4] = NEW_LINE; - e++; - } - e += 4; - } - - if (BuildConfig.DEBUG && e != outBuff.length) - throw new RuntimeException(); - return outBuff; - } - - /* ******** D E C O D I N G M E T H O D S ******** */ - - /** - * Decodes four bytes from array <var>source</var> - * and writes the resulting bytes (up to three of them) - * to <var>destination</var>. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * <var>srcOffset</var> and <var>destOffset</var>. - * This method does not check to make sure your arrays - * are large enough to accommodate <var>srcOffset</var> + 4 for - * the <var>source</var> array or <var>destOffset</var> + 3 for - * the <var>destination</var> array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param decodabet the decodabet for decoding Base64 content - * @return the number of decoded bytes converted - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, byte[] decodabet) { - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); - - destination[destOffset] = (byte)(outBuff >>> 16); - return 1; - } else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Example: DkL= - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); - - destination[destOffset] = (byte)(outBuff >>> 16); - destination[destOffset + 1] = (byte)(outBuff >>> 8); - return 2; - } else { - // Example: DkLE - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); - - destination[destOffset] = (byte)(outBuff >> 16); - destination[destOffset + 1] = (byte)(outBuff >> 8); - destination[destOffset + 2] = (byte)(outBuff); - return 3; - } - } // end decodeToBytes - - /** - * Decodes data from Base64 notation. - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decode(bytes, 0, bytes.length); - } - - /** - * Decodes data from web safe Base64 notation. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decodeWebSafe(bytes, 0, bytes.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source) throws Base64DecoderException { - return decode(source, 0, source.length); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded data. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(byte[] source) - throws Base64DecoderException { - return decodeWebSafe(source, 0, source.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, DECODABET); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded byte array. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - */ - public static byte[] decodeWebSafe(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, WEBSAFE_DECODABET); - } - - /** - * Decodes Base64 content using the supplied decodabet and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param decodabet the decodabet for decoding Base64 content - * @return decoded data - */ - public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) - throws Base64DecoderException { - int len34 = len * 3 / 4; - byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for (i = 0; i < len; i++) { - sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits - sbiDecode = decodabet[sbiCrop]; - - if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better - if (sbiDecode >= EQUALS_SIGN_ENC) { - // An equals sign (for padding) must not occur at position 0 or 1 - // and must be the last byte[s] in the encoded value - if (sbiCrop == EQUALS_SIGN) { - int bytesLeft = len - i; - byte lastByte = (byte)(source[len - 1 + off] & 0x7f); - if (b4Posn == 0 || b4Posn == 1) { - throw new Base64DecoderException( - "invalid padding byte '=' at byte offset " + i); - } else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) { - throw new Base64DecoderException( - "padding byte '=' falsely signals end of encoded value " - + "at offset " + i); - } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { - throw new Base64DecoderException( - "encoded value has invalid trailing byte"); - } - break; - } - - b4[b4Posn++] = sbiCrop; - if (b4Posn == 4) { - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - b4Posn = 0; - } - } - } else { - throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)"); - } - } - - // Because web safe encoding allows non padding base64 encodes, we - // need to pad the rest of the b4 buffer with equal signs when - // b4Posn != 0. There can be at most 2 equal signs at the end of - // four characters, so the b4 buffer must have two or three - // characters. This also catches the case where the input is - // padded with EQUALS_SIGN - if (b4Posn != 0) { - if (b4Posn == 1) { - throw new Base64DecoderException("single trailing character at offset " + (len - 1)); - } - b4[b4Posn++] = EQUALS_SIGN; - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - } - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } -} diff --git a/platform/android/java/src/org/godotengine/godot/GodotLib.java b/platform/android/java/src/org/godotengine/godot/GodotLib.java deleted file mode 100644 index 81c98bcc79..0000000000 --- a/platform/android/java/src/org/godotengine/godot/GodotLib.java +++ /dev/null @@ -1,76 +0,0 @@ -/*************************************************************************/ -/* GodotLib.java */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2019 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; - -// Wrapper for native library - -public class GodotLib { - - public static GodotIO io; - - static { - System.loadLibrary("godot_android"); - } - - /** - * @param width the current view width - * @param height the current view height - */ - - public static native void initialize(Godot p_instance, Object p_asset_manager, boolean use_apk_expansion); - public static native void ondestroy(Godot p_instance); - public static native void setup(String[] p_cmdline); - public static native void resize(int width, int height); - public static native void newcontext(boolean p_32_bits); - public static native void back(); - public static native void step(); - public static native void touch(int what, int pointer, int howmany, int[] arr); - public static native void accelerometer(float x, float y, float z); - public static native void gravity(float x, float y, float z); - public static native void magnetometer(float x, float y, float z); - public static native void gyroscope(float x, float y, float z); - public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed); - public static native void joybutton(int p_device, int p_but, boolean p_pressed); - public static native void joyaxis(int p_device, int p_axis, float p_value); - public static native void joyhat(int p_device, int p_hat_x, int p_hat_y); - public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name); - public static native void focusin(); - public static native void focusout(); - public static native void audio(); - public static native void singleton(String p_name, Object p_object); - public static native void method(String p_sname, String p_name, String p_ret, String[] p_params); - public static native String getGlobal(String p_key); - public static native void callobject(int p_id, String p_method, Object[] p_params); - public static native void calldeferred(int p_id, String p_method, Object[] p_params); - public static native void requestPermissionResult(String p_permission, boolean p_result); - - public static native void setVirtualKeyboardHeight(int p_height); -} |