diff options
Diffstat (limited to 'platform/android/java/lib')
127 files changed, 14916 insertions, 0 deletions
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/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl new file mode 100644 index 0000000000..869cb16f68 --- /dev/null +++ b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl @@ -0,0 +1,21 @@ +/* + * 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.android.vending.licensing; + +oneway interface ILicenseResultListener { + void verifyLicense(int responseCode, String signedData, String signature); +} diff --git a/platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl new file mode 100644 index 0000000000..9541a2090c --- /dev/null +++ b/platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl @@ -0,0 +1,23 @@ +/* + * 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.android.vending.licensing; + +import com.android.vending.licensing.ILicenseResultListener; + +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..6d07504e45 --- /dev/null +++ b/platform/android/java/lib/build.gradle @@ -0,0 +1,91 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation "com.android.support:support-core-utils:28.0.0" +} + +def pathToRootDir = "../../../../" +// Note: Only keep the abis you support to speed up the gradle 'assemble' task. +def supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] + +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 + + // Create tasks to generate the Godot native libraries. + def taskName = "compileGodotNativeLibs" + buildType + def releaseTarget = "release" + if (buildType == "Debug") { + releaseTarget += "_debug" + } + + def abiTaskNames = [] + // Creating gradle tasks to generate the native libraries for the supported abis. + supportedAbis.each { abi -> + def abiTaskName = taskName + abi.capitalize() + abiTaskNames += abiTaskName + tasks.create(name: abiTaskName, type: Exec) { + executable "scons" + args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${abi}" + } + } + + // Creating gradle task to run all of the previously generated tasks. + tasks.create(name: taskName, type: GradleBuild) { + tasks = abiTaskNames + } + + // 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..2c246b04a4 --- /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..8bcd464bed --- /dev/null +++ b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-nodpi/icon.png b/platform/android/java/lib/res/drawable-nodpi/icon.png Binary files differnew file mode 100644 index 0000000000..6ad9b43117 --- /dev/null +++ b/platform/android/java/lib/res/drawable-nodpi/icon.png diff --git a/platform/android/java/lib/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 differnew file mode 100644 index 0000000000..372b763ec5 --- /dev/null +++ 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..b458ff3057 --- /dev/null +++ b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/layout/downloading_expansion.xml b/platform/android/java/lib/res/layout/downloading_expansion.xml new file mode 100644 index 0000000000..4a9700965f --- /dev/null +++ b/platform/android/java/lib/res/layout/downloading_expansion.xml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:orientation="vertical" > + + <TextView + android:id="@+id/statusText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:layout_marginStart="5dp" + android:layout_marginTop="10dp" + android:textStyle="bold" /> + + <LinearLayout + android:id="@+id/downloaderDashboard" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" > + + <TextView + android:id="@+id/progressAsFraction" + style="@android:style/TextAppearance.Small" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_marginStart="5dp" /> + + <TextView + android:id="@+id/progressAsPercentage" + style="@android:style/TextAppearance.Small" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignEnd="@+id/progressBar" /> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/progressAsFraction" + android:layout_marginBottom="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:layout_marginTop="10dp" /> + + <TextView + android:id="@+id/progressAverageSpeed" + style="@android:style/TextAppearance.Small" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_below="@+id/progressBar" + android:layout_marginStart="5dp" /> + + <TextView + android:id="@+id/progressTimeRemaining" + style="@android:style/TextAppearance.Small" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignEnd="@+id/progressBar" + android:layout_below="@+id/progressBar" /> + </RelativeLayout> + + <LinearLayout + android:id="@+id/downloadButton" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <Button + android:id="@+id/cancelButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginBottom="10dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:layout_marginTop="10dp" + android:layout_weight="0" + android:minHeight="40dp" + android:minWidth="94dp" + android:text="@string/text_button_cancel" + android:visibility="gone" + style="?android:attr/buttonBarButtonStyle" /> + + <Button + android:id="@+id/pauseButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginBottom="10dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="5dp" + android:layout_marginTop="10dp" + android:layout_weight="0" + android:minHeight="40dp" + android:minWidth="94dp" + android:text="@string/text_button_pause" + style="?android:attr/buttonBarButtonStyle" /> + </LinearLayout> + </LinearLayout> + </LinearLayout> + + <LinearLayout + android:id="@+id/approveCellular" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" + android:visibility="gone" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:id="@+id/textPausedParagraph1" + android:text="@string/text_paused_cellular" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:id="@+id/textPausedParagraph2" + android:text="@string/text_paused_cellular_2" /> + + <LinearLayout + android:id="@+id/buttonRow" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <Button + android:id="@+id/resumeOverCellular" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_margin="10dp" + android:text="@string/text_button_resume_cellular" + style="?android:attr/buttonBarButtonStyle" /> + + <Button + android:id="@+id/wifiSettingsButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_margin="10dp" + android:text="@string/text_button_wifi_settings" + style="?android:attr/buttonBarButtonStyle" /> + </LinearLayout> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml new file mode 100644 index 0000000000..fae1faeb60 --- /dev/null +++ b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2008, 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. +*/ +--> + +<LinearLayout xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:baselineAligned="false" + android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android"> + + <RelativeLayout + android:layout_width="35dp" + android:layout_height="fill_parent" + android:paddingTop="10dp" + android:paddingBottom="8dp" > + + <ImageView + android:id="@+id/appIcon" + android:layout_width="fill_parent" + android:layout_height="25dp" + android:scaleType="centerInside" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:src="@android:drawable/stat_sys_download" + android:contentDescription="@string/godot_project_name_string" /> + + <TextView + android:id="@+id/progress_text" + style="@style/NotificationText" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentBottom="true" + android:layout_gravity="center_horizontal" + android:singleLine="true" + android:gravity="center" /> + </RelativeLayout> + + <RelativeLayout + android:layout_width="0dip" + android:layout_height="match_parent" + android:layout_weight="1.0" + android:clickable="true" + android:focusable="true" + android:paddingTop="10dp" + android:paddingEnd="8dp" + android:paddingBottom="8dp" + tools:ignore="RtlSymmetry"> + + <TextView + android:id="@+id/title" + style="@style/NotificationTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:singleLine="true"/> + + <TextView + android:id="@+id/time_remaining" + style="@style/NotificationText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:singleLine="true" + tools:ignore="RelativeOverlap" /> + <!-- Only one of progress_bar and paused_text will be visible. --> + + <FrameLayout + android:id="@+id/progress_bar_frame" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" > + + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingEnd="25dp" /> + + <TextView + android:id="@+id/description" + style="@style/NotificationTextShadow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingEnd="25dp" + android:singleLine="true" /> + </FrameLayout> + + </RelativeLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ar/strings.xml b/platform/android/java/lib/res/values-ar/strings.xml new file mode 100644 index 0000000000..9f3dc6d6ac --- /dev/null +++ b/platform/android/java/lib/res/values-ar/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ar</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-bg/strings.xml b/platform/android/java/lib/res/values-bg/strings.xml new file mode 100644 index 0000000000..bd8109277e --- /dev/null +++ b/platform/android/java/lib/res/values-bg/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-bg</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ca/strings.xml b/platform/android/java/lib/res/values-ca/strings.xml new file mode 100644 index 0000000000..494cb88468 --- /dev/null +++ b/platform/android/java/lib/res/values-ca/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ca</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-cs/strings.xml b/platform/android/java/lib/res/values-cs/strings.xml new file mode 100644 index 0000000000..30ce00f895 --- /dev/null +++ b/platform/android/java/lib/res/values-cs/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-cs</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-da/strings.xml b/platform/android/java/lib/res/values-da/strings.xml new file mode 100644 index 0000000000..4c2a1cf0f4 --- /dev/null +++ b/platform/android/java/lib/res/values-da/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-da</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-de/strings.xml b/platform/android/java/lib/res/values-de/strings.xml new file mode 100644 index 0000000000..52946d4cce --- /dev/null +++ b/platform/android/java/lib/res/values-de/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-de</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-el/strings.xml b/platform/android/java/lib/res/values-el/strings.xml new file mode 100644 index 0000000000..181dc51762 --- /dev/null +++ b/platform/android/java/lib/res/values-el/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-el</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-en/strings.xml b/platform/android/java/lib/res/values-en/strings.xml new file mode 100644 index 0000000000..976a565013 --- /dev/null +++ b/platform/android/java/lib/res/values-en/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-en</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-es-rES/strings.xml b/platform/android/java/lib/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..73f63a08f8 --- /dev/null +++ b/platform/android/java/lib/res/values-es-rES/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-es_ES</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-es/strings.xml b/platform/android/java/lib/res/values-es/strings.xml new file mode 100644 index 0000000000..07b718a641 --- /dev/null +++ b/platform/android/java/lib/res/values-es/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-es</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-fa/strings.xml b/platform/android/java/lib/res/values-fa/strings.xml new file mode 100644 index 0000000000..f1e29013c4 --- /dev/null +++ b/platform/android/java/lib/res/values-fa/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-fa</string> + <string name="text_paused_cellular">آیا می خواهید بر روی اتصال داده همراه دانلود را شروع کنید؟ بر اساس نوع سطح داده شما این ممکن است برای شما هزینه مالی داشته باشد.</string> + <string name="text_paused_cellular_2">اگر نمی خواهید بر روی اتصال داده همراه دانلود را شروع کنید ، دانلود به صورت خودکار در زمان دسترسی به وای-فای شروع می شود.</string> + <string name="text_button_resume_cellular">ادامه دانلود</string> + <string name="text_button_wifi_settings">تنظیمات وای-فای</string> + <string name="text_verifying_download">درحال تایید دانلود</string> + <string name="text_validation_complete">تایید فایل XAPK تکمیل شد. برای خروج تایید کنید.</string> + <string name="text_validation_failed">اعتبارسنجی فایل XAPK ناموق.</string> + <string name="text_button_pause">توقف دانلود</string> + <string name="text_button_resume">ادامه دانلود</string> + <string name="text_button_cancel">انصراف</string> + <string name="text_button_cancel_verify">انصراف از تایید شدن</string> +</resources> diff --git a/platform/android/java/lib/res/values-fi/strings.xml b/platform/android/java/lib/res/values-fi/strings.xml new file mode 100644 index 0000000000..323d82aff1 --- /dev/null +++ b/platform/android/java/lib/res/values-fi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-fi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-fr/strings.xml b/platform/android/java/lib/res/values-fr/strings.xml new file mode 100644 index 0000000000..32bead2661 --- /dev/null +++ b/platform/android/java/lib/res/values-fr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-fr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hi/strings.xml b/platform/android/java/lib/res/values-hi/strings.xml new file mode 100644 index 0000000000..8aab2a8c63 --- /dev/null +++ b/platform/android/java/lib/res/values-hi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-hi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hr/strings.xml b/platform/android/java/lib/res/values-hr/strings.xml new file mode 100644 index 0000000000..caf55e2241 --- /dev/null +++ b/platform/android/java/lib/res/values-hr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-hr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hu/strings.xml b/platform/android/java/lib/res/values-hu/strings.xml new file mode 100644 index 0000000000..e7f9e51226 --- /dev/null +++ b/platform/android/java/lib/res/values-hu/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-hu</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-in/strings.xml b/platform/android/java/lib/res/values-in/strings.xml new file mode 100644 index 0000000000..9e9a8b0c03 --- /dev/null +++ b/platform/android/java/lib/res/values-in/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-id</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-it/strings.xml b/platform/android/java/lib/res/values-it/strings.xml new file mode 100644 index 0000000000..1f5e5a049e --- /dev/null +++ b/platform/android/java/lib/res/values-it/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-it</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-iw/strings.xml b/platform/android/java/lib/res/values-iw/strings.xml new file mode 100644 index 0000000000..f52ede2085 --- /dev/null +++ b/platform/android/java/lib/res/values-iw/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-he</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ja/strings.xml b/platform/android/java/lib/res/values-ja/strings.xml new file mode 100644 index 0000000000..7f85f57df7 --- /dev/null +++ b/platform/android/java/lib/res/values-ja/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ja</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ko/strings.xml b/platform/android/java/lib/res/values-ko/strings.xml new file mode 100644 index 0000000000..fab0bdd753 --- /dev/null +++ b/platform/android/java/lib/res/values-ko/strings.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ko</string> + <string name="text_paused_cellular">모바일 네트워크를 사용하여 다운로드 하시겠습니까? 남은 데이터 사용량에 따라, 요금이 부과될 수 있습니다.</string> + <string name="text_paused_cellular_2">모바일 네트워크를 사용하여 다운로드 하지 않을 경우, 와이파이 연결이 가능할 때 자동적으로 다운로드가 이루어집니다.</string> + <string name="text_button_resume_cellular">다운로드 계속하기</string> + <string name="text_button_wifi_settings">와이파이 설정</string> + <string name="text_verifying_download">다운로드 확인중</string> + <string name="text_validation_complete">추가 파일 확인이 완료되었습니다. 확인을 눌러 진행하세요.</string> + <string name="text_validation_failed">추가 파일 확인에 실패하였습니다.</string> + <string name="text_button_pause">다운로드 일시정지</string> + <string name="text_button_resume">다운로드 계속하기</string> + <string name="text_button_cancel">취소</string> + <string name="text_button_cancel_verify">파일 확인 취소</string> + + <!-- APK Expansion Strings --> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download successfully completed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_complete">다운로드 완료</string> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download failed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_failed">다운로드 실패</string> + + + <string name="state_unknown">시작중…</string> + <string name="state_idle">다운로드 시작을 기다리는 중</string> + <string name="state_fetching_url">다운로드할 항목을 찾는 중</string> + <string name="state_connecting">다운로드 서버에 연결 중</string> + <string name="state_downloading">다운로드 중</string> + <string name="state_completed">다운로드 종료</string> + <string name="state_paused_network_unavailable">와이파이를 찾을 수 없어 다운로드가 일시정지 되었습니다.</string> + <string name="state_paused_network_setup_failure">다운로드가 일시정지 되었습니다. 네트워크 연결 상태를 확인하세요.</string> + <string name="state_paused_by_request">다운로드 일시정지</string> + <string name="state_paused_wifi_unavailable">와이파이가 사용하능하지 않아 다운로드가 일시정지 되었습니다.</string> + <string name="state_paused_wifi_disabled">와이파이가 비활성화 되어 다운로드가 일시정지 되었습니다.</string> + <string name="state_paused_roaming">로밍 상태이어서 다운로드가 일시정지 되었습니다.</string> + <string name="state_paused_sdcard_unavailable">외부 저장소를 사용할 수 없어 다운로드가 일시정지 되었습니다.</string> + <string name="state_failed_unlicensed">이 앱을 구매하지 않아 다운로드가 정지 되었습니다.</string> + <string name="state_failed_fetching_url">다운로드 항목을 찾을 수 없어 다운로드가 정지 되었습니다.</string> + <string name="state_failed_sdcard_full">외부 저장소가 가득차서 다운로드가 실패하였습니다.</string> + <string name="state_failed_cancelled">다운로드 취소</string> + <string name="state_failed">다운로드 실패</string> + + <string name="kilobytes_per_second">%1$s KB/s</string> + <string name="time_remaining">남은 시간: %1$s</string> + <string name="time_remaining_notification">%1$s 남음</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-lt/strings.xml b/platform/android/java/lib/res/values-lt/strings.xml new file mode 100644 index 0000000000..6e3677fde7 --- /dev/null +++ b/platform/android/java/lib/res/values-lt/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-lt</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-lv/strings.xml b/platform/android/java/lib/res/values-lv/strings.xml new file mode 100644 index 0000000000..701fc271ac --- /dev/null +++ b/platform/android/java/lib/res/values-lv/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-lv</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-nb/strings.xml b/platform/android/java/lib/res/values-nb/strings.xml new file mode 100644 index 0000000000..73147ca1af --- /dev/null +++ b/platform/android/java/lib/res/values-nb/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-nb</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-nl/strings.xml b/platform/android/java/lib/res/values-nl/strings.xml new file mode 100644 index 0000000000..e501928a35 --- /dev/null +++ b/platform/android/java/lib/res/values-nl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-nl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-pl/strings.xml b/platform/android/java/lib/res/values-pl/strings.xml new file mode 100644 index 0000000000..ea5da73b6f --- /dev/null +++ b/platform/android/java/lib/res/values-pl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-pl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-pt/strings.xml b/platform/android/java/lib/res/values-pt/strings.xml new file mode 100644 index 0000000000..bdda7cd2c7 --- /dev/null +++ b/platform/android/java/lib/res/values-pt/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-pt</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ro/strings.xml b/platform/android/java/lib/res/values-ro/strings.xml new file mode 100644 index 0000000000..3686da4c19 --- /dev/null +++ b/platform/android/java/lib/res/values-ro/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ro</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ru/strings.xml b/platform/android/java/lib/res/values-ru/strings.xml new file mode 100644 index 0000000000..954067658b --- /dev/null +++ b/platform/android/java/lib/res/values-ru/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-ru</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sk/strings.xml b/platform/android/java/lib/res/values-sk/strings.xml new file mode 100644 index 0000000000..37d1283124 --- /dev/null +++ b/platform/android/java/lib/res/values-sk/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-sk</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sl/strings.xml b/platform/android/java/lib/res/values-sl/strings.xml new file mode 100644 index 0000000000..0bb249c375 --- /dev/null +++ b/platform/android/java/lib/res/values-sl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-sl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sr/strings.xml b/platform/android/java/lib/res/values-sr/strings.xml new file mode 100644 index 0000000000..0e83cab1a1 --- /dev/null +++ b/platform/android/java/lib/res/values-sr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-sr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sv/strings.xml b/platform/android/java/lib/res/values-sv/strings.xml new file mode 100644 index 0000000000..e3a04ac2ec --- /dev/null +++ b/platform/android/java/lib/res/values-sv/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-sv</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-th/strings.xml b/platform/android/java/lib/res/values-th/strings.xml new file mode 100644 index 0000000000..0aa893b8bf --- /dev/null +++ b/platform/android/java/lib/res/values-th/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-th</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-tl/strings.xml b/platform/android/java/lib/res/values-tl/strings.xml new file mode 100644 index 0000000000..e7e2af4909 --- /dev/null +++ b/platform/android/java/lib/res/values-tl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-tl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-tr/strings.xml b/platform/android/java/lib/res/values-tr/strings.xml new file mode 100644 index 0000000000..97af1243a6 --- /dev/null +++ b/platform/android/java/lib/res/values-tr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-tr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-uk/strings.xml b/platform/android/java/lib/res/values-uk/strings.xml new file mode 100644 index 0000000000..3dea6908a9 --- /dev/null +++ b/platform/android/java/lib/res/values-uk/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-uk</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-vi/strings.xml b/platform/android/java/lib/res/values-vi/strings.xml new file mode 100644 index 0000000000..a6552130b0 --- /dev/null +++ b/platform/android/java/lib/res/values-vi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-vi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-zh-rCN/strings.xml b/platform/android/java/lib/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..6668c56bd9 --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rCN/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-zh</string> +</resources> diff --git a/platform/android/java/lib/res/values-zh-rHK/strings.xml b/platform/android/java/lib/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..8a6269da0f --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rHK/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-zh_HK</string> +</resources> diff --git a/platform/android/java/lib/res/values-zh-rTW/strings.xml b/platform/android/java/lib/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..b1bb39d5d6 --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rTW/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name-zh_TW</string> +</resources> diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml new file mode 100644 index 0000000000..a1b81a6186 --- /dev/null +++ b/platform/android/java/lib/res/values/strings.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_project_name_string">godot-project-name</string> + <string name="text_paused_cellular">Would you like to enable downloading over cellular connections? Depending on your data plan, this may cost you money.</string> + <string name="text_paused_cellular_2">If you choose not to enable downloading over cellular connections, the download will automatically resume when wi-fi is available.</string> + <string name="text_button_resume_cellular">Resume download</string> + <string name="text_button_wifi_settings">Wi-Fi settings</string> + <string name="text_verifying_download">Verifying Download</string> + <string name="text_validation_complete">XAPK File Validation Complete. Select OK to exit.</string> + <string name="text_validation_failed">XAPK File Validation Failed.</string> + <string name="text_button_pause">Pause Download</string> + <string name="text_button_resume">Resume Download</string> + <string name="text_button_cancel">Cancel</string> + <string name="text_button_cancel_verify">Cancel Verification</string> + + <!-- APK Expansion Strings --> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download successfully completed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_complete">Download complete</string> + + <!-- When a download completes, a notification is displayed, and this + string is used to indicate that the download failed. + Note that such a download could have been initiated by a variety of + applications, including (but not limited to) the browser, an email + application, a content marketplace. --> + <string name="notification_download_failed">Download unsuccessful</string> + + + <string name="state_unknown">Starting…</string> + <string name="state_idle">Waiting for download to start</string> + <string name="state_fetching_url">Looking for resources to download</string> + <string name="state_connecting">Connecting to the download server</string> + <string name="state_downloading">Downloading resources</string> + <string name="state_completed">Download finished</string> + <string name="state_paused_network_unavailable">Download paused because no network is available</string> + <string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string> + <string name="state_paused_by_request">Download paused</string> + <string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string> + <string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string> + <string name="state_paused_roaming">Download paused because you are roaming</string> + <string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string> + <string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string> + <string name="state_failed_fetching_url">Download failed because the resources could not be found</string> + <string name="state_failed_sdcard_full">Download failed because the external storage is full</string> + <string name="state_failed_cancelled">Download cancelled</string> + <string name="state_failed">Download failed</string> + + <string name="kilobytes_per_second">%1$s KB/s</string> + <string name="time_remaining">Time remaining: %1$s</string> + <string name="time_remaining_notification">%1$s left</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values/styles.xml b/platform/android/java/lib/res/values/styles.xml new file mode 100644 index 0000000000..a442f61e7e --- /dev/null +++ b/platform/android/java/lib/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="NotificationText"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> + + <style name="NotificationTextShadow" parent="NotificationText"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:shadowColor">@android:color/background_dark</item> + <item name="android:shadowDx">1.0</item> + <item name="android:shadowDy">1.0</item> + <item name="android:shadowRadius">1</item> + </style> + + <style name="NotificationTitle"> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textStyle">bold</item> + </style> + + <style name="ButtonBackground"> + <item name="android:background">@android:color/background_dark</item> + </style> + +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java new file mode 100644 index 0000000000..1dcc370d83 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java @@ -0,0 +1,236 @@ +/* + * 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 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"; + + /** + * Expansion path where we store obb files + */ + public static final String EXP_PATH = File.separator + "Android" + + File.separator + "obb" + File.separator; + + /** The intent that gets sent when the service must wake up for a retry */ + public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; + + /** the intent that gets sent when clicking a successful download */ + public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; + + /** the intent that gets sent when clicking 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"; + + /** + * 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 = "-"; + + /** 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 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 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; + + /** + * 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 + + /** + * 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 + + /** + * The maximum number of redirects. + */ + 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; + + /** Enable separate connectivity logging */ + public static final boolean LOGX = true; + + /** 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; + + /** + * 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; + + /** + * 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; + + /** + * This download can't be performed because the content type cannot be + * handled. + */ + 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 + * a content length but don't have one, and it is also used in the + * client when a response is received whose length cannot be determined + * accurately (therefore making it impossible to know when a download + * completes). + */ + 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; + + /** + * The lowest-valued error status that is not an actual HTTP status code. + */ + 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; + + /** + * Some possibly transient error occurred, but we can't resume the download. + */ + public static final int STATUS_CANNOT_RESUME = 489; + + /** + * This download was canceled + */ + 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. + */ + 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; + + /** + * This download couldn't be completed because of an + * unspecified unhandled HTTP code. + */ + 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; + + /** + * This download couldn't be completed because of an + * HttpException while setting up the request. + */ + 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; + + /** + * 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; + + /** + * 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; + + /** + * The wake duration to check to see if a download is possible. + */ + 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; + +}
\ 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/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java new file mode 100644 index 0000000000..cef3794701 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -0,0 +1,126 @@ +/* + * 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.Messenger; + +/** + * This interface should be implemented by the client activity for the + * 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_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; + + /** + * 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 + * Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not + * available. + * <p> + * The service does not return these values. We recommend that app + * 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_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_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 = 19; + + /** + * Called internally by the stub when the service is bound to the client. + * <p> + * Critical implementation detail. In onServiceConnected we create the + * remote service and marshaler. This is how we pass the client information + * back to the service so the client can be properly notified of changes. We + * must do this every time we reconnect to the service. + * <p> + * That is, when you receive this callback, you should call + * {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member + * instance of {@link IDownloaderService}, then call + * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved + * from your {@link IStub} proxy object. + * + * @param m the service Messenger. This Messenger is used to call the + * service API from the client. + */ + void onServiceConnected(Messenger m); + + /** + * 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. + * <p> + * The Downloader Library includes a collection of string resources that + * correspond to each of the states, which you can use to provide users a + * useful message based on the state provided in this callback. To fetch the + * appropriate string for a state, call + * {@link Helpers#getDownloaderStringResourceIDFromState}. + * <p> + * What this means to the developer: The application has gotten a message + * that the download has paused due to lack of WiFi. The developer should + * then show UI asking the user if they want to enable downloading over + * cellular connections with appropriate warnings. If the application + * suddenly starts downloading, the application should revert to showing the + * progress again, rather than leaving up the download over cellular UI up. + * + * @param newState one of the STATE_* values defined in IDownloaderClient + */ + void onDownloadStateChanged(int newState); + + /** + * Shows the download progress. This is intended to be used to fill out a + * client UI. This progress should only be shown in a few states such as + * STATE_DOWNLOADING. + * + * @param progress the DownloadProgressInfo object containing the current + * progress of all downloads. + */ + void onDownloadProgress(DownloadProgressInfo progress); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java new file mode 100644 index 0000000000..4de9de0c62 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -0,0 +1,83 @@ +/* + * 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.os.Messenger; + +/** + * This interface is implemented by the DownloaderService and by the + * DownloaderServiceMarshaller. It contains functions to control the service. + * When a client binds to the service, it must call the onClientUpdated + * function. + * <p> + * You can acquire a proxy that implements this interface for your service by + * calling {@link DownloaderServiceMarshaller#CreateProxy} during the + * {@link IDownloaderClient#onServiceConnected} callback. At which point, you + * 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; + + /** + * Request that the service abort the current download. The service should + * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. + */ + 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(); + + /** + * Request that the service continue a paused download, when in any paused + * or failed state, including + * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. + */ + void requestContinueDownload(); + + /** + * Set the flags for this download (e.g. + * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). + * + * @param flags + */ + void setDownloadFlags(int flags); + + /** + * Requests that the download status be sent to the client. + */ + void requestDownloadStatus(); + + /** + * Call this when you get {@link + * IDownloaderClient.onServiceConnected(Messenger m)} from the + * DownloaderClient to register the client with the service. It will + * automatically send the current status to the client. + * + * @param clientMessenger + */ + void onClientUpdated(Messenger clientMessenger); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java new file mode 100644 index 0000000000..d5bc3a843e --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java @@ -0,0 +1,41 @@ +/* + * 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.content.Context; +import android.os.Messenger; + +/** + * This is the interface that is used to connect/disconnect from the downloader + * service. + * <p> + * You should get a proxy object that implements this interface by calling + * {@link DownloaderClientMarshaller#CreateStub} in your activity when the + * downloader service starts. Then, call {@link #connect} during your activity's + * onResume() and call {@link #disconnect} during onStop(). + * <p> + * Then during the {@link IDownloaderClient#onServiceConnected} callback, you + * should call {@link #getMessenger} to pass the stub's Messenger object to + * {@link IDownloaderService#onClientUpdated}. + */ +public interface IStub { + Messenger getMessenger(); + + void connect(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/lib/src/com/google/android/vending/licensing/DeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java new file mode 100644 index 0000000000..e5c5e2d7ca --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * Allows the developer to limit the number of devices using a single license. + * <p> + * The LICENSED response from the server contains a user identifier unique to + * the <application, user> pair. The developer can send this identifier + * to their own server along with some device identifier (a random number + * generated and stored once per application installation, + * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId}, + * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc). + * The more sources used to identify the device, the harder it will be for an + * attacker to spoof. + * <p> + * The server can look at the <application, user, device id> tuple and + * restrict a user's application license to run on at most 10 different devices + * in a week (for example). We recommend not being too restrictive because a + * user might legitimately have multiple devices or be in the process of + * changing phones. This will catch egregious violations of multiple people + * sharing one license. + */ +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); +} 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/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java new file mode 100644 index 0000000000..8b869ddaaf --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -0,0 +1,67 @@ +/* + * 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; + +/** + * Callback for the license checker library. + * <p> + * Upon checking with the Market server and conferring with the {@link Policy}, + * the library calls the appropriate callback method to communicate the result. + * <p> + * <b>The callback does not occur in the original checking thread.</b> Your + * application should post to the appropriate handling thread or lock + * accordingly. + * <p> + * The reason that is passed back with allow/dontAllow is the base status handed + * to the policy for allowed/disallowing the license. Policy.RETRY will call + * allow or dontAllow depending on other statistics associated with the policy, + * while in most cases Policy.NOT_LICENSED will call dontAllow and + * Policy.LICENSED will Allow. + */ +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); + + /** + * 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); + + /** 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); +} 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/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java new file mode 100644 index 0000000000..d87af3153f --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * A DeviceLimiter that doesn't limit the number of devices that can use a + * given user's license. + * <p> + * Unless you have reason to believe that your application is being pirated + * by multiple users using the same license (signing in to Market as the same + * user), we recommend you use this implementation. + */ +public class NullDeviceLimiter implements DeviceLimiter { + + public int isDeviceAllowed(String userId) { + return Policy.LICENSED; + } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java new file mode 100644 index 0000000000..008c150a8e --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Interface used as part of a {@link Policy} to allow application authors to obfuscate + * licensing data that will be stored into a SharedPreferences file. + * <p> + * Any transformation scheme must be reversable. Implementing classes may optionally implement an + * integrity check to further prevent modification to preference data. Implementing classes + * should use device-specific information as a key in the obfuscation algorithm to prevent + * obfuscated preferences from being shared among devices. + */ +public interface Obfuscator { + + /** + * Obfuscate a string that is being stored into shared preferences. + * + * @param original The data that is to be obfuscated. + * @param key The key for the data that is to be obfuscated. + * @return A transformed version of the original data. + */ + String obfuscate(String original, String key); + + /** + * Undo the transformation applied to data by the obfuscate() method. + * + * @param obfuscated The data that is to be un-obfuscated. + * @param key The key for the data that is to be un-obfuscated. + * @return The original data transformed by the obfuscate() method. + * @throws ValidationException Optionally thrown if a data integrity check fails. + */ + String unobfuscate(String obfuscated, String key) throws ValidationException; +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java new file mode 100644 index 0000000000..b672a078b7 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** + * Policy used by {@link LicenseChecker} to determine whether a user should have + * access to the application. + */ +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; + /** + * 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; + /** + * RETRY means that the license response was unable to be determined --- + * perhaps as a result of faulty networking + */ + public static final int RETRY = 0x0123; + + /** + * Provide results from contact with the license server. Retry counts are + * incremented if the current value of response is RETRY. Results will be + * used for any future policy decisions. + * + * @param response the result from validating the server response + * @param rawData the raw server response data, can be null for RETRY + */ + void processServerResponse(int response, ResponseData rawData); + + /** + * Check if the user should be allowed access to the application. + */ + boolean allowAccess(); + + /** + * Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g. + * buy app on the Play Store). + */ + String getLicensingUrl(); +} diff --git a/platform/android/java/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/lib/src/com/google/android/vending/licensing/StrictPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java new file mode 100644 index 0000000000..c2d55c37f1 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.util.Log; +import com.google.android.vending.licensing.util.URIQueryDecoder; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** + * Non-caching policy. All requests will be sent to the licensing service, + * and no local caching is performed. + * <p> + * Using a non-caching policy ensures that there is no local preference data + * for malicious users to tamper with. As a side effect, applications + * will not be permitted to run while offline. Developers should carefully + * weigh the risks of using this Policy over one which implements caching, + * such as ServerManagedPolicy. + * <p> + * Access to the application is only allowed if a LICENSED response is. + * received. All other responses (including RETRY) will deny access. + */ +public class StrictPolicy implements Policy { + + private static final String TAG = "StrictPolicy"; + + private int mLastResponse; + private String mLicensingUrl; + + public StrictPolicy() { + // Set default policy. This will force the application to check the policy on launch. + mLastResponse = Policy.RETRY; + 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 + * extra is still extracted in cases where the app is unlicensed. + * + * @param response the result from validating the server response + * @param rawData the raw server response data + */ + public void processServerResponse(int response, ResponseData rawData) { + mLastResponse = response; + + 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 String getLicensingUrl() { + return mLicensingUrl; + } + + private Map<String, String> decodeExtras( + com.google.android.vending.licensing.ResponseData rawData) { + Map<String, String> results = new HashMap<String, String>(); + if (rawData == null) { + return results; + } + + try { + URI rawExtras = new URI("?" + rawData.extra); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java new file mode 100644 index 0000000000..ee4df47c68 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Indicates that an error occurred while validating the integrity of data managed by an + * {@link Obfuscator}.} + */ +public class ValidationException extends Exception { + public ValidationException() { + super(); + } + + public ValidationException(String s) { + super(s); + } + + 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/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java new file mode 100644 index 0000000000..1aef1b54b8 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// 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; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000000..5155bf5ac3 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing.util; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Map; +import java.util.Scanner; + +public class URIQueryDecoder { + private static final String TAG = "URIQueryDecoder"; + + /** + * Decodes the query portion of the passed-in URI. + * + * @param encodedURI the URI containing the query to decode + * @param results a map containing all query parameters. Query parameters that do not have a + * value will map to a null string + */ + static public void DecodeQuery(URI encodedURI, Map<String, String> results) { + Scanner scanner = new Scanner(encodedURI.getRawQuery()); + scanner.useDelimiter("&"); + try { + while (scanner.hasNext()) { + String param = scanner.next(); + String[] valuePair = param.split("="); + String name, value; + if (valuePair.length == 1) { + value = null; + } else if (valuePair.length == 2) { + value = URLDecoder.decode(valuePair[1], "UTF-8"); + } else { + throw new IllegalArgumentException("query parameter invalid"); + } + name = URLDecoder.decode(valuePair[0], "UTF-8"); + results.put(name, value); + } + } catch (UnsupportedEncodingException e) { + // This should never happen. + Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error."); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java new file mode 100644 index 0000000000..588d9ae646 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java @@ -0,0 +1,81 @@ +/*************************************************************************/ +/* Dictionary.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 java.util.HashMap; +import java.util.Set; + +public class Dictionary extends HashMap<String, Object> { + + protected String[] keys_cache; + + public String[] get_keys() { + + String[] ret = new String[size()]; + int i = 0; + Set<String> keys = keySet(); + for (String key : keys) { + + ret[i] = key; + i++; + }; + + return ret; + }; + + public Object[] get_values() { + + Object[] ret = new Object[size()]; + int i = 0; + Set<String> keys = keySet(); + for (String key : keys) { + + ret[i] = get(key); + i++; + }; + + return ret; + }; + + public void set_keys(String[] keys) { + keys_cache = keys; + }; + + public void set_values(Object[] vals) { + + int i = 0; + for (String key : keys_cache) { + put(key, vals[i]); + i++; + }; + keys_cache = null; + }; +}; diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java new file mode 100644 index 0000000000..7e852b7e08 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -0,0 +1,1111 @@ +/*************************************************************************/ +/* Godot.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.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.ConfigurationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Messenger; +import android.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; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +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 java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import javax.microedition.khronos.opengles.GL10; +import org.godotengine.godot.input.GodotEditText; +import org.godotengine.godot.payments.PaymentsManager; +import org.godotengine.godot.xr.XRMode; + +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 TextView mStatusText; + private TextView mProgressFraction; + private TextView mProgressPercent; + private TextView mAverageSpeed; + private TextView mTimeRemaining; + private ProgressBar mPB; + private ClipboardManager mClipboard; + + private View mDashboard; + private View mCellMessage; + + private Button mPauseButton; + private Button mWiFiSettingsButton; + + private XRMode xrMode = XRMode.REGULAR; + private boolean use_32_bits = false; + private boolean use_immersive = false; + private boolean use_debug_opengl = false; + private boolean mStatePaused; + private boolean activityResumed; + private int mState; + + static private Intent mCurrentIntent; + + @Override + public void onNewIntent(Intent intent) { + mCurrentIntent = intent; + } + + static public Intent getCurrentIntent() { + return mCurrentIntent; + } + + private void setState(int newState) { + if (mState != newState) { + mState = newState; + mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState)); + } + } + + private void setButtonPausedState(boolean paused) { + mStatePaused = paused; + int stringResourceID = paused ? R.string.text_button_resume : + R.string.text_button_pause; + mPauseButton.setText(stringResourceID); + } + + static public class SingletonBase { + + protected void registerClass(String p_name, String[] p_methods) { + + GodotLib.singleton(p_name, this); + + Class clazz = getClass(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + boolean found = false; + + for (String s : p_methods) { + if (s.equals(method.getName())) { + found = true; + break; + } + } + if (!found) + continue; + + List<String> ptr = new ArrayList<String>(); + + Class[] paramTypes = method.getParameterTypes(); + for (Class c : paramTypes) { + ptr.add(c.getName()); + } + + String[] pt = new String[ptr.size()]; + ptr.toArray(pt); + + GodotLib.method(p_name, method.getName(), method.getReturnType().getName(), pt); + } + + Godot.singletons[Godot.singleton_count++] = this; + } + + protected void onMainActivityResult(int requestCode, int resultCode, Intent data) { + } + + protected void onMainRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + } + + protected void onMainPause() {} + protected void onMainResume() {} + protected void onMainDestroy() {} + protected boolean onMainBackPressed() { return false; } + + protected void onGLDrawFrame(GL10 gl) {} + protected void onGLSurfaceChanged(GL10 gl, int width, int height) {} // singletons will always miss first onGLSurfaceChanged call + //protected void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} // singletons won't be ready until first GodotLib.step() + + public void registerMethods() {} + } + + /* + protected List<SingletonBase> singletons = new ArrayList<SingletonBase>(); + protected void instanceSingleton(SingletonBase s) { + + s.registerMethods(); + singletons.add(s); + } + */ + + private String[] command_line; + private boolean use_apk_expansion; + + public GodotView mView; + private boolean godot_initialized = false; + + private SensorManager mSensorManager; + private Sensor mAccelerometer; + private Sensor mGravity; + private Sensor mMagnetometer; + private Sensor mGyroscope; + + public static GodotIO io; + + static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS]; + static int singleton_count = 0; + + public interface ResultCallback { + public void callback(int requestCode, int resultCode, Intent data); + } + public ResultCallback result_callback; + + private PaymentsManager mPaymentsManager = null; + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == PaymentsManager.REQUEST_CODE_FOR_PURCHASE) { + mPaymentsManager.processPurchaseResponse(resultCode, data); + } else if (result_callback != null) { + result_callback.callback(requestCode, resultCode, data); + result_callback = null; + }; + + for (int i = 0; i < singleton_count; i++) { + + singletons[i].onMainActivityResult(requestCode, resultCode, data); + } + }; + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (int i = 0; i < singleton_count; i++) { + singletons[i].onMainRequestPermissionsResult(requestCode, permissions, grantResults); + } + + for (int i = 0; i < permissions.length; i++) { + GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED); + } + }; + + /** + * 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; + + final FrameLayout layout = new FrameLayout(this); + layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setContentView(layout); + + // GodotEditText layout + GodotEditText edittext = new GodotEditText(this); + edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + // ...add to FrameLayout + layout.addView(edittext); + + mView = new GodotView(this, xrMode, use_gl3, use_32_bits, use_debug_opengl); + layout.addView(mView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + edittext.setView(mView); + io.setEdit(edittext); + + final Godot godot = this; + mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Point fullSize = new Point(); + godot.getWindowManager().getDefaultDisplay().getSize(fullSize); + Rect gameSize = new Rect(); + godot.mView.getWindowVisibleDisplayFrame(gameSize); + + final int keyboardHeight = fullSize.y - gameSize.bottom; + GodotLib.setVirtualKeyboardHeight(keyboardHeight); + } + }); + + final String[] current_command_line = command_line; + mView.queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.setup(current_command_line); + setKeepScreenOn("True".equals(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); + } + }); + } + + public void setKeepScreenOn(final boolean p_enabled) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (p_enabled) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + }); + } + + /** + * 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: + // + // Currently it's very hard to properly deinitialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing statics). + // + // Using instrumentation is a way of making the whole app process restart, because Android + // will kill any process of the same package which was already running. + // + Bundle args = new Bundle(); + args.putParcelable("intent", mCurrentIntent); + startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args); + } + + public void alert(final String message, final String title) { + final Activity activity = this; + runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(message).setTitle(title); + builder.setPositiveButton( + "OK", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + }); + } + + public int getGLESVersionCode() { + ActivityManager am = (ActivityManager)this.getSystemService(Context.ACTIVITY_SERVICE); + ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo(); + return deviceInfo.reqGlEsVersion; + } + + private String[] getCommandLine() { + InputStream is; + try { + is = getAssets().open("_cl_"); + byte[] len = new byte[4]; + int r = is.read(len); + if (r < 4) { + return new String[0]; + } + int argc = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); + String[] cmdline = new String[argc]; + + for (int i = 0; i < argc; i++) { + r = is.read(len); + if (r < 4) { + + return new String[0]; + } + int strlen = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); + if (strlen > 65535) { + return new String[0]; + } + byte[] arg = new byte[strlen]; + r = is.read(arg); + if (r == strlen) { + cmdline[i] = new String(arg, "UTF-8"); + } + } + return cmdline; + } catch (Exception e) { + e.printStackTrace(); + return new String[0]; + } + } + + /** + * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused. + */ + @Keep + private boolean isActivityResumed() { + return activityResumed; + } + + /** + * 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() { + + if (expansion_pack_path != null) { + + String[] new_cmdline; + int cll = 0; + if (command_line != null) { + new_cmdline = new String[command_line.length + 2]; + cll = command_line.length; + for (int i = 0; i < command_line.length; i++) { + new_cmdline[i] = command_line[i]; + } + } else { + new_cmdline = new String[2]; + } + + new_cmdline[cll] = "--main-pack"; + new_cmdline[cll + 1] = expansion_pack_path; + command_line = new_cmdline; + } + + io = new GodotIO(this); + io.unique_id = Secure.getString(getContentResolver(), Secure.ANDROID_ID); + GodotLib.io = io; + mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); + mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); + mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); + mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); + mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); + + GodotLib.initialize(this, getAssets(), use_apk_expansion); + + result_callback = null; + + mPaymentsManager = PaymentsManager.createManager(this).initService(); + + godot_initialized = true; + } + + @Override + public void onServiceConnected(Messenger m) { + IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); + remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); + } + + @Override + protected void onCreate(Bundle icicle) { + + super.onCreate(icicle); + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + mClipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); + + //check for apk expansion API + if (true) { + boolean md5mismatch = false; + command_line = getCommandLine(); + String main_pack_md5 = null; + String main_pack_key = null; + + List<String> new_args = new LinkedList<String>(); + + for (int i = 0; i < command_line.length; i++) { + + boolean has_extra = i < command_line.length - 1; + if (command_line[i].equals(XRMode.REGULAR.cmdLineArg)) { + xrMode = XRMode.REGULAR; + } else if (command_line[i].equals(XRMode.OVR.cmdLineArg)) { + xrMode = XRMode.OVR; + } else if (command_line[i].equals("--use_depth_32")) { + use_32_bits = true; + } else if (command_line[i].equals("--debug_opengl")) { + use_debug_opengl = true; + } else if (command_line[i].equals("--use_immersive")) { + use_immersive = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + + UiChangeListener(); + } + } else if (command_line[i].equals("--use_apk_expansion")) { + use_apk_expansion = true; + } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { + main_pack_md5 = command_line[i + 1]; + i++; + } else if (has_extra && command_line[i].equals("--apk_expansion_key")) { + main_pack_key = command_line[i + 1]; + SharedPreferences prefs = getSharedPreferences("app_data_keys", MODE_PRIVATE); + Editor editor = prefs.edit(); + editor.putString("store_public_key", main_pack_key); + + editor.apply(); + i++; + } else if (command_line[i].trim().length() != 0) { + new_args.add(command_line[i]); + } + } + + if (new_args.isEmpty()) { + command_line = null; + } else { + + command_line = new_args.toArray(new String[new_args.size()]); + } + if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) { + //check that environment is ok! + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + //show popup and die + } + + // Build the full path to the app's expansion files + try { + expansion_pack_path = Helpers.getSaveFilePath(getApplicationContext()); + expansion_pack_path += "/main." + getPackageManager().getPackageInfo(getPackageName(), 0).versionCode + "." + this.getPackageName() + ".obb"; + } catch (Exception e) { + e.printStackTrace(); + } + + File f = new File(expansion_pack_path); + + boolean pack_valid = true; + + if (!f.exists()) { + + pack_valid = false; + + } else if (obbIsCorrupted(expansion_pack_path, main_pack_md5)) { + pack_valid = false; + try { + f.delete(); + } catch (Exception e) { + } + } + + if (!pack_valid) { + + Intent notifierIntent = new Intent(this, this.getClass()); + notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + int startResult; + try { + startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired( + getApplicationContext(), + pendingIntent, + GodotDownloaderService.class); + + if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { + // This is where you do set up to display the download + // progress (next step) + mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, + GodotDownloaderService.class); + + 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; + } + } catch (NameNotFoundException e) { + // TODO Auto-generated catch block + } + } + } + } + + mCurrentIntent = getIntent(); + + initializeGodot(); + } + + @Override + protected void onDestroy() { + + if (mPaymentsManager != null) mPaymentsManager.destroy(); + for (int i = 0; i < singleton_count; i++) { + singletons[i].onMainDestroy(); + } + + 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 + protected void onPause() { + super.onPause(); + activityResumed = false; + + if (!godot_initialized) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.disconnect(this); + } + return; + } + mView.onPause(); + mView.queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.focusout(); + } + }); + mSensorManager.unregisterListener(this); + + for (int i = 0; i < singleton_count; i++) { + singletons[i].onMainPause(); + } + } + + public String getClipboard() { + + String copiedText = ""; + + if (mClipboard.getPrimaryClip() != null) { + ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0); + copiedText = item.getText().toString(); + } + + return copiedText; + } + + public void setClipboard(String p_text) { + + ClipData clip = ClipData.newPlainText("myLabel", p_text); + mClipboard.setPrimaryClip(clip); + } + + @Override + protected void onResume() { + super.onResume(); + if (!godot_initialized) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.connect(this); + } + return; + } + + 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); + mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); + + if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + Window window = getWindow(); + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + for (int i = 0; i < singleton_count; i++) { + + singletons[i].onMainResume(); + } + + activityResumed = true; + } + + public void UiChangeListener() { + final View decorView = getWindow().getDecorView(); + decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + } + } + }); + } + + @Override + public void onSensorChanged(SensorEvent event) { + Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); + int displayRotation = display.getRotation(); + + float[] adjustedValues = new float[3]; + final int axisSwap[][] = { + { 1, -1, 0, 1 }, // ROTATION_0 + { -1, -1, 1, 0 }, // ROTATION_90 + { -1, 1, 0, 1 }, // ROTATION_180 + { 1, 1, 1, 0 } + }; // ROTATION_270 + + final int[] as = axisSwap[displayRotation]; + adjustedValues[0] = (float)as[0] * event.values[as[2]]; + adjustedValues[1] = (float)as[1] * event.values[as[3]]; + adjustedValues[2] = event.values[2]; + + final float x = adjustedValues[0]; + final float y = adjustedValues[1]; + final float z = adjustedValues[2]; + + final int typeOfSensor = event.sensor.getType(); + if (mView != null) { + mView.queueEvent(new Runnable() { + @Override + public void run() { + if (typeOfSensor == Sensor.TYPE_ACCELEROMETER) { + GodotLib.accelerometer(-x, y, -z); + } + if (typeOfSensor == Sensor.TYPE_GRAVITY) { + GodotLib.gravity(-x, y, -z); + } + if (typeOfSensor == Sensor.TYPE_MAGNETIC_FIELD) { + GodotLib.magnetometer(-x, y, -z); + } + if (typeOfSensor == Sensor.TYPE_GYROSCOPE) { + GodotLib.gyroscope(x, -y, z); + } + } + }); + } + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + // Do something here if sensor accuracy changes. + } + + /* + @Override public boolean dispatchKeyEvent(KeyEvent event) { + + if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) { + + System.out.printf("** BACK REQUEST!\n"); + + GodotLib.quit(); + return true; + } + System.out.printf("** OTHER KEY!\n"); + + return false; + } + */ + + @Override + public void onBackPressed() { + boolean shouldQuit = true; + + for (int i = 0; i < singleton_count; i++) { + if (singletons[i].onMainBackPressed()) { + shouldQuit = false; + } + } + + if (shouldQuit && mView != null) { + mView.queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.back(); + } + }); + } + } + + private void forceQuit() { + System.exit(0); + } + + private boolean obbIsCorrupted(String f, String main_pack_md5) { + + try { + + InputStream fis = new FileInputStream(f); + + // Create MD5 Hash + byte[] buffer = new byte[16384]; + + MessageDigest complete = MessageDigest.getInstance("MD5"); + int numRead; + do { + numRead = fis.read(buffer); + if (numRead > 0) { + complete.update(buffer, 0, numRead); + } + } while (numRead != -1); + + fis.close(); + byte[] messageDigest = complete.digest(); + + // Create Hex String + StringBuffer hexString = new StringBuffer(); + for (int i = 0; i < messageDigest.length; i++) { + String s = Integer.toHexString(0xFF & messageDigest[i]); + + if (s.length() == 1) { + s = "0" + s; + } + hexString.append(s); + } + String md5str = hexString.toString(); + + if (!md5str.equals(main_pack_md5)) { + return true; + } + return false; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + public boolean gotTouchEvent(final MotionEvent event) { + + final int evcount = event.getPointerCount(); + if (evcount == 0) + return true; + + if (mView != null) { + final int[] arr = new int[event.getPointerCount() * 3]; + + for (int i = 0; i < event.getPointerCount(); i++) { + + arr[i * 3 + 0] = (int)event.getPointerId(i); + arr[i * 3 + 1] = (int)event.getX(i); + arr[i * 3 + 2] = (int)event.getY(i); + } + final int pointer_idx = event.getPointerId(event.getActionIndex()); + + //System.out.printf("gaction: %d\n",event.getAction()); + final int action = event.getAction() & MotionEvent.ACTION_MASK; + mView.queueEvent(new Runnable() { + @Override + public void run() { + switch (action) { + case MotionEvent.ACTION_DOWN: { + GodotLib.touch(0, 0, evcount, arr); + //System.out.printf("action down at: %f,%f\n", event.getX(),event.getY()); + } break; + case MotionEvent.ACTION_MOVE: { + GodotLib.touch(1, 0, evcount, arr); + /* + for(int i=0;i<event.getPointerCount();i++) { + System.out.printf("%d - moved to: %f,%f\n",i, event.getX(i),event.getY(i)); + } + */ + } break; + case MotionEvent.ACTION_POINTER_UP: { + GodotLib.touch(4, pointer_idx, evcount, arr); + //System.out.printf("%d - s.up at: %f,%f\n",pointer_idx, event.getX(pointer_idx),event.getY(pointer_idx)); + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + GodotLib.touch(3, pointer_idx, evcount, arr); + //System.out.printf("%d - s.down at: %f,%f\n",pointer_idx, event.getX(pointer_idx),event.getY(pointer_idx)); + } break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + GodotLib.touch(2, 0, evcount, arr); + /* + for(int i=0;i<event.getPointerCount();i++) { + System.out.printf("%d - up! %f,%f\n",i, event.getX(i),event.getY(i)); + } + */ + } break; + } + } + }); + } + return true; + } + + @Override + public boolean onKeyMultiple(final int inKeyCode, int repeatCount, KeyEvent event) { + String s = event.getCharacters(); + if (s == null || s.length() == 0) + return super.onKeyMultiple(inKeyCode, repeatCount, event); + + final char[] cc = s.toCharArray(); + int cnt = 0; + for (int i = cc.length; --i >= 0; cnt += cc[i] != 0 ? 1 : 0) + ; + if (cnt == 0) return super.onKeyMultiple(inKeyCode, repeatCount, event); + 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++) { + int keyCode; + if ((keyCode = cc[i]) != 0) { + // Simulate key down and up... + GodotLib.key(0, keyCode, true); + GodotLib.key(0, keyCode, false); + } + } + } + }); + return true; + } + + public PaymentsManager getPaymentsManager() { + return mPaymentsManager; + } + + public boolean requestPermission(String p_name) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Not necessary, asked on install already + return true; + } + + if (p_name.equals("RECORD_AUDIO")) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); + return false; + } + } + + if (p_name.equals("CAMERA")) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); + 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; + } + + /** + * The download state should trigger changes in the UI --- it may be useful + * to show the state as being indeterminate at times. This sample can be + * considered a guideline. + */ + @Override + public void onDownloadStateChanged(int newState) { + setState(newState); + boolean showDashboard = true; + boolean showCellMessage = false; + boolean paused; + boolean indeterminate; + switch (newState) { + case IDownloaderClient.STATE_IDLE: + // STATE_IDLE means the service is listening, so it's + // safe to start making remote service calls. + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_CONNECTING: + case IDownloaderClient.STATE_FETCHING_URL: + showDashboard = true; + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_DOWNLOADING: + paused = false; + showDashboard = true; + indeterminate = false; + break; + + case IDownloaderClient.STATE_FAILED_CANCELED: + case IDownloaderClient.STATE_FAILED: + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + case IDownloaderClient.STATE_FAILED_UNLICENSED: + paused = true; + showDashboard = false; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: + showDashboard = false; + paused = true; + indeterminate = false; + showCellMessage = true; + break; + + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_ROAMING: + case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_COMPLETED: + showDashboard = false; + paused = false; + indeterminate = false; + initializeGodot(); + return; + default: + paused = true; + indeterminate = true; + showDashboard = true; + } + int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE; + if (mDashboard.getVisibility() != newDashboardVisibility) { + mDashboard.setVisibility(newDashboardVisibility); + } + int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE; + if (mCellMessage.getVisibility() != cellMessageVisibility) { + mCellMessage.setVisibility(cellMessageVisibility); + } + + mPB.setIndeterminate(indeterminate); + setButtonPausedState(paused); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + mAverageSpeed.setText(getString(R.string.kilobytes_per_second, + Helpers.getSpeedString(progress.mCurrentSpeed))); + mTimeRemaining.setText(getString(R.string.time_remaining, + Helpers.getTimeRemaining(progress.mTimeRemaining))); + + mPB.setMax((int)(progress.mOverallTotal >> 8)); + mPB.setProgress((int)(progress.mOverallProgress >> 8)); + mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); + mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, + progress.mOverallTotal)); + } + public void initInputDevices() { + mView.initInputDevices(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java new file mode 100644 index 0000000000..e7e2a3f808 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java @@ -0,0 +1,59 @@ +/*************************************************************************/ +/* GodotDownloaderAlarmReceiver.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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; + +/** + * You should start your derived downloader class when this receiver gets the message + * from the alarm service using the provided service helper function within the + * DownloaderClientMarshaller. This class must be then registered in your AndroidManifest.xml + * file with a section like this: + * <receiver android:name=".GodotDownloaderAlarmReceiver"/> + */ +public class GodotDownloaderAlarmReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Log.d("GODOT", "Alarma recivida"); + try { + DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, GodotDownloaderService.class); + } catch (NameNotFoundException e) { + e.printStackTrace(); + Log.d("GODOT", "Exception: " + e.getClass().getName() + ":" + e.getMessage()); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java new file mode 100644 index 0000000000..8e10710c9f --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java @@ -0,0 +1,84 @@ +/*************************************************************************/ +/* GodotDownloaderService.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.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +/** + * This class demonstrates the minimal client implementation of the + * DownloaderService from the Downloader library. + */ +public class GodotDownloaderService extends DownloaderService { + // stuff for LVL -- MODIFY FOR YOUR APPLICATION! + private static final String BASE64_PUBLIC_KEY = "REPLACE THIS WITH YOUR PUBLIC KEY"; + // used by the preference obfuscater + private static final byte[] SALT = new byte[] { + 1, 43, -12, -1, 54, 98, + -100, -12, 43, 2, -8, -4, 9, 5, -106, -108, -33, 45, -1, 84 + }; + + /** + * This public key comes from your Android Market publisher account, and it + * used by the LVL to validate responses from Market on your behalf. + */ + @Override + public String getPublicKey() { + SharedPreferences prefs = getApplicationContext().getSharedPreferences("app_data_keys", Context.MODE_PRIVATE); + Log.d("GODOT", "getting public key:" + prefs.getString("store_public_key", null)); + return prefs.getString("store_public_key", null); + + //return BASE64_PUBLIC_KEY; + } + + /** + * This is used by the preference obfuscater to make sure that your + * obfuscated preferences are different than the ones used by other + * applications. + */ + @Override + public byte[] getSALT() { + return SALT; + } + + /** + * Fill this in with the class name for your alarm receiver. We do this + * because receivers must be unique across all of Android (it's a good idea + * to make sure that your receiver is in your unique package) + */ + @Override + public String getAlarmReceiverClassName() { + Log.d("GODOT", "getAlarmReceiverClassName()"); + return GodotDownloaderAlarmReceiver.class.getName(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java new file mode 100644 index 0000000000..04566cf62c --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -0,0 +1,631 @@ +/*************************************************************************/ +/* GodotIO.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.content.*; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.AssetManager; +import android.media.*; +import android.net.Uri; +import android.os.*; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import org.godotengine.godot.input.*; +//android.os.Build + +// Wrapper for native library + +public class GodotIO { + + AssetManager am; + Godot activity; + GodotEditText edit; + + MediaPlayer mediaPlayer; + + final int SCREEN_LANDSCAPE = 0; + final int SCREEN_PORTRAIT = 1; + final int SCREEN_REVERSE_LANDSCAPE = 2; + final int SCREEN_REVERSE_PORTRAIT = 3; + final int SCREEN_SENSOR_LANDSCAPE = 4; + final int SCREEN_SENSOR_PORTRAIT = 5; + final int SCREEN_SENSOR = 6; + + ///////////////////////// + /// FILES + ///////////////////////// + + public int last_file_id = 1; + + class AssetData { + + public boolean eof = false; + public String path; + public InputStream is; + public int len; + public int pos; + } + + SparseArray<AssetData> streams; + + public int file_open(String path, boolean write) { + + //System.out.printf("file_open: Attempt to Open %s\n",path); + + //Log.v("MyApp", "TRYING TO OPEN FILE: " + path); + if (write) + return -1; + + AssetData ad = new AssetData(); + + try { + ad.is = am.open(path); + + } catch (Exception e) { + + //System.out.printf("Exception on file_open: %s\n",path); + return -1; + } + + try { + ad.len = ad.is.available(); + } catch (Exception e) { + + System.out.printf("Exception availabling on file_open: %s\n", path); + return -1; + } + + ad.path = path; + ad.pos = 0; + ++last_file_id; + streams.put(last_file_id, ad); + + return last_file_id; + } + public int file_get_size(int id) { + + if (streams.get(id) == null) { + System.out.printf("file_get_size: Invalid file id: %d\n", id); + return -1; + } + + return streams.get(id).len; + } + public void file_seek(int id, int bytes) { + + if (streams.get(id) == null) { + System.out.printf("file_get_size: Invalid file id: %d\n", id); + return; + } + //seek sucks + AssetData ad = streams.get(id); + if (bytes > ad.len) + bytes = ad.len; + if (bytes < 0) + bytes = 0; + + try { + + if (bytes > (int)ad.pos) { + int todo = bytes - (int)ad.pos; + while (todo > 0) { + todo -= ad.is.skip(todo); + } + ad.pos = bytes; + } else if (bytes < (int)ad.pos) { + + ad.is = am.open(ad.path); + + ad.pos = bytes; + int todo = bytes; + while (todo > 0) { + todo -= ad.is.skip(todo); + } + } + + ad.eof = false; + } catch (IOException e) { + + System.out.printf("Exception on file_seek: %s\n", e); + return; + } + } + + public int file_tell(int id) { + + if (streams.get(id) == null) { + System.out.printf("file_read: Can't tell eof for invalid file id: %d\n", id); + return 0; + } + + AssetData ad = streams.get(id); + return ad.pos; + } + public boolean file_eof(int id) { + + if (streams.get(id) == null) { + System.out.printf("file_read: Can't check eof for invalid file id: %d\n", id); + return false; + } + + AssetData ad = streams.get(id); + return ad.eof; + } + + public byte[] file_read(int id, int bytes) { + + if (streams.get(id) == null) { + System.out.printf("file_read: Can't read invalid file id: %d\n", id); + return new byte[0]; + } + + AssetData ad = streams.get(id); + + if (ad.pos + bytes > ad.len) { + + bytes = ad.len - ad.pos; + ad.eof = true; + } + + if (bytes == 0) { + + return new byte[0]; + } + + byte[] buf1 = new byte[bytes]; + int r = 0; + try { + r = ad.is.read(buf1); + } catch (IOException e) { + + System.out.printf("Exception on file_read: %s\n", e); + return new byte[bytes]; + } + + if (r == 0) { + return new byte[0]; + } + + ad.pos += r; + + if (r < bytes) { + + byte[] buf2 = new byte[r]; + for (int i = 0; i < r; i++) + buf2[i] = buf1[i]; + return buf2; + } else { + + return buf1; + } + } + + public void file_close(int id) { + + if (streams.get(id) == null) { + System.out.printf("file_close: Can't close invalid file id: %d\n", id); + return; + } + + streams.remove(id); + } + + ///////////////////////// + /// DIRECTORIES + ///////////////////////// + + class AssetDir { + + public String[] files; + public int current; + public String path; + } + + public int last_dir_id = 1; + + SparseArray<AssetDir> dirs; + + public int dir_open(String path) { + + AssetDir ad = new AssetDir(); + ad.current = 0; + ad.path = path; + + try { + ad.files = am.list(path); + // no way to find path is directory or file exactly. + // but if ad.files.length==0, then it's an empty directory or file. + if (ad.files.length == 0) { + return -1; + } + } catch (IOException e) { + + System.out.printf("Exception on dir_open: %s\n", e); + return -1; + } + + //System.out.printf("Opened dir: %s\n",path); + ++last_dir_id; + dirs.put(last_dir_id, ad); + + return last_dir_id; + } + + public boolean dir_is_dir(int id) { + if (dirs.get(id) == null) { + System.out.printf("dir_next: invalid dir id: %d\n", id); + return false; + } + AssetDir ad = dirs.get(id); + //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); + int idx = ad.current; + if (idx > 0) + idx--; + + if (idx >= ad.files.length) + return false; + String fname = ad.files[idx]; + + try { + if (ad.path.equals("")) + am.open(fname); + else + am.open(ad.path + "/" + fname); + return false; + } catch (Exception e) { + return true; + } + } + + public String dir_next(int id) { + + if (dirs.get(id) == null) { + System.out.printf("dir_next: invalid dir id: %d\n", id); + return ""; + } + + AssetDir ad = dirs.get(id); + //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); + + if (ad.current >= ad.files.length) { + ad.current++; + return ""; + } + String r = ad.files[ad.current]; + ad.current++; + return r; + } + + public void dir_close(int id) { + + if (dirs.get(id) == null) { + System.out.printf("dir_close: invalid dir id: %d\n", id); + return; + } + + dirs.remove(id); + } + + GodotIO(Godot p_activity) { + + am = p_activity.getAssets(); + activity = p_activity; + //streams = new HashMap<Integer, AssetData>(); + streams = new SparseArray<AssetData>(); + dirs = new SparseArray<AssetDir>(); + } + + ///////////////////////// + // AUDIO + ///////////////////////// + + private Object buf; + private Thread mAudioThread; + private AudioTrack mAudioTrack; + + public Object audioInit(int sampleRate, int desiredFrames) { + int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + int audioFormat = AudioFormat.ENCODING_PCM_16BIT; + int frameSize = 4; + + System.out.printf("audioInit: initializing audio:\n"); + + //Log.v("Godot", "Godot audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + ((float)sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + // Let the user pick a larger buffer if they really want -- but ye + // gods they probably shouldn't, the minimums are horrifyingly high + // latency already + desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); + + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, + channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); + + audioStartThread(); + + //Log.v("Godot", "Godot audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + ((float)mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); + + buf = new short[desiredFrames * 2]; + return buf; + } + + public void audioStartThread() { + mAudioThread = new Thread(new Runnable() { + public void run() { + mAudioTrack.play(); + GodotLib.audio(); + } + }); + + // I'd take REALTIME if I could get it! + mAudioThread.setPriority(Thread.MAX_PRIORITY); + mAudioThread.start(); + } + + public void audioWriteShortBuffer(short[] buffer) { + for (int i = 0; i < buffer.length;) { + int result = mAudioTrack.write(buffer, i, buffer.length - i); + if (result > 0) { + i += result; + } else if (result == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Nom nom + } + } else { + Log.w("Godot", "Godot audio: error return from write(short)"); + return; + } + } + } + + public void audioQuit() { + if (mAudioThread != null) { + try { + mAudioThread.join(); + } catch (Exception e) { + Log.v("Godot", "Problem stopping audio thread: " + e); + } + mAudioThread = null; + + //Log.v("Godot", "Finished waiting for audio thread"); + } + + if (mAudioTrack != null) { + mAudioTrack.stop(); + mAudioTrack = null; + } + } + + public void audioPause(boolean p_pause) { + + if (p_pause) + mAudioTrack.pause(); + else + mAudioTrack.play(); + } + + ///////////////////////// + // MISCELLANEOUS OS IO + ///////////////////////// + + public int openURI(String p_uri) { + + try { + Log.v("MyApp", "TRYING TO OPEN URI: " + p_uri); + String path = p_uri; + String type = ""; + if (path.startsWith("/")) { + //absolute path to filesystem, prepend file:// + path = "file://" + path; + if (p_uri.endsWith(".png") || p_uri.endsWith(".jpg") || p_uri.endsWith(".gif") || p_uri.endsWith(".webp")) { + + type = "image/*"; + } + } + + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + if (!type.equals("")) { + intent.setDataAndType(Uri.parse(path), type); + } else { + intent.setData(Uri.parse(path)); + } + + activity.startActivity(intent); + return 0; + } catch (ActivityNotFoundException e) { + + return 1; + } + } + + public String getDataDir() { + + return activity.getFilesDir().getAbsolutePath(); + } + + public String getLocale() { + + return Locale.getDefault().toString(); + } + + public String getModel() { + return Build.MODEL; + } + + public int getScreenDPI() { + DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); + return (int)(metrics.density * 160f); + } + + public void showKeyboard(String p_existing_text) { + if (edit != null) + edit.showKeyboard(p_existing_text); + + //InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); + //inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + }; + + public void hideKeyboard() { + if (edit != null) + edit.hideKeyboard(); + }; + + public void setScreenOrientation(int p_orientation) { + + switch (p_orientation) { + + case SCREEN_LANDSCAPE: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } break; + case SCREEN_PORTRAIT: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } break; + case SCREEN_REVERSE_LANDSCAPE: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + } break; + case SCREEN_REVERSE_PORTRAIT: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); + } break; + case SCREEN_SENSOR_LANDSCAPE: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } break; + case SCREEN_SENSOR_PORTRAIT: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } break; + case SCREEN_SENSOR: { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + } break; + } + }; + + public void setEdit(GodotEditText _edit) { + edit = _edit; + } + + public void playVideo(String p_path) { + Uri filePath = Uri.parse(p_path); + mediaPlayer = new MediaPlayer(); + + try { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setDataSource(activity.getApplicationContext(), filePath); + mediaPlayer.prepare(); + mediaPlayer.start(); + } catch (IOException e) { + System.out.println("IOError while playing video"); + } + } + + public boolean isVideoPlaying() { + if (mediaPlayer != null) { + return mediaPlayer.isPlaying(); + } + return false; + } + + public void pauseVideo() { + if (mediaPlayer != null) { + mediaPlayer.pause(); + } + } + + public void stopVideo() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + + public static final int SYSTEM_DIR_DESKTOP = 0; + public static final int SYSTEM_DIR_DCIM = 1; + public static final int SYSTEM_DIR_DOCUMENTS = 2; + public static final int SYSTEM_DIR_DOWNLOADS = 3; + public static final int SYSTEM_DIR_MOVIES = 4; + public static final int SYSTEM_DIR_MUSIC = 5; + public static final int SYSTEM_DIR_PICTURES = 6; + public static final int SYSTEM_DIR_RINGTONES = 7; + + public String getSystemDir(int idx) { + + String what = ""; + switch (idx) { + case SYSTEM_DIR_DESKTOP: { + //what=Environment.DIRECTORY_DOCUMENTS; + what = Environment.DIRECTORY_DOWNLOADS; + } break; + case SYSTEM_DIR_DCIM: { + what = Environment.DIRECTORY_DCIM; + + } break; + case SYSTEM_DIR_DOCUMENTS: { + what = Environment.DIRECTORY_DOWNLOADS; + //what=Environment.DIRECTORY_DOCUMENTS; + } break; + case SYSTEM_DIR_DOWNLOADS: { + what = Environment.DIRECTORY_DOWNLOADS; + + } break; + case SYSTEM_DIR_MOVIES: { + what = Environment.DIRECTORY_MOVIES; + + } break; + case SYSTEM_DIR_MUSIC: { + what = Environment.DIRECTORY_MUSIC; + } break; + case SYSTEM_DIR_PICTURES: { + what = Environment.DIRECTORY_PICTURES; + } break; + case SYSTEM_DIR_RINGTONES: { + what = Environment.DIRECTORY_RINGTONES; + + } break; + } + + if (what.equals("")) + return ""; + return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath(); + } + + protected static final String PREFS_FILE = "device_id.xml"; + protected static final String PREFS_DEVICE_ID = "device_id"; + + public static String unique_id = ""; + public String getUniqueID() { + + return unique_id; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java new file mode 100644 index 0000000000..0466f380e8 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java @@ -0,0 +1,50 @@ +/*************************************************************************/ +/* GodotInstrumentation.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.Instrumentation; +import android.content.Intent; +import android.os.Bundle; + +public class GodotInstrumentation extends Instrumentation { + private Intent intent; + + @Override + public void onCreate(Bundle arguments) { + intent = arguments.getParcelable("intent"); + start(); + } + + @Override + public void onStart() { + startActivitySync(intent); + } +} 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..af51c840cb --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -0,0 +1,214 @@ +/*************************************************************************/ +/* 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 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); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java b/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java new file mode 100644 index 0000000000..1432cd3a67 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java @@ -0,0 +1,230 @@ +/*************************************************************************/ +/* GodotPaymentV3.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.util.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.godotengine.godot.payments.PaymentsManager; +import org.json.JSONException; +import org.json.JSONObject; + +public class GodotPaymentV3 extends Godot.SingletonBase { + + private Godot activity; + private Integer purchaseCallbackId = 0; + private String accessToken; + private String purchaseValidationUrlPrefix; + private String transactionId; + private PaymentsManager mPaymentManager; + private Dictionary mSkuDetails = new Dictionary(); + + public void purchase(final String sku, final String transactionId) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + mPaymentManager.requestPurchase(sku, transactionId); + } + }); + } + + static public Godot.SingletonBase initialize(Activity p_activity) { + + return new GodotPaymentV3(p_activity); + } + + public GodotPaymentV3(Activity p_activity) { + + registerClass("GodotPayments", new String[] { "purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected" }); + activity = (Godot)p_activity; + mPaymentManager = activity.getPaymentsManager(); + mPaymentManager.setBaseSingleton(this); + } + + public void consumeUnconsumedPurchases() { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + mPaymentManager.consumeUnconsumedPurchases(); + } + }); + } + + private String signature; + + public String getSignature() { + return this.signature; + } + + public void callbackSuccess(String ticket, String signature, String sku) { + GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku }); + } + + public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) { + Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku); + GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku }); + } + + public void callbackSuccessNoUnconsumedPurchases() { + GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {}); + } + + public void callbackFailConsume(String message) { + GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message }); + } + + public void callbackFail(String message) { + GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message }); + } + + public void callbackCancel() { + GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {}); + } + + public void callbackAlreadyOwned(String sku) { + GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku }); + } + + public int getPurchaseCallbackId() { + return purchaseCallbackId; + } + + public void setPurchaseCallbackId(int purchaseCallbackId) { + this.purchaseCallbackId = purchaseCallbackId; + } + + public String getPurchaseValidationUrlPrefix() { + return this.purchaseValidationUrlPrefix; + } + + public void setPurchaseValidationUrlPrefix(String url) { + this.purchaseValidationUrlPrefix = url; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public String getTransactionId() { + return this.transactionId; + } + + // request purchased items are not consumed + public void requestPurchased() { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + mPaymentManager.requestPurchased(); + } + }); + } + + // callback for requestPurchased() + public void callbackPurchased(String receipt, String signature, String sku) { + GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku }); + } + + public void callbackDisconnected() { + GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {}); + } + + public void callbackConnected() { + GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {}); + } + + // true if connected, false otherwise + public boolean isConnected() { + return mPaymentManager.isConnected(); + } + + // consume item automatically after purchase. default is true. + public void setAutoConsume(boolean autoConsume) { + mPaymentManager.setAutoConsume(autoConsume); + } + + // consume a specific item + public void consume(String sku) { + mPaymentManager.consume(sku); + } + + // query in app item detail info + public void querySkuDetails(String[] list) { + List<String> nKeys = Arrays.asList(list); + List<String> cKeys = Arrays.asList(mSkuDetails.get_keys()); + ArrayList<String> fKeys = new ArrayList<String>(); + for (String key : nKeys) { + if (!cKeys.contains(key)) { + fKeys.add(key); + } + } + if (fKeys.size() > 0) { + mPaymentManager.querySkuDetails(fKeys.toArray(new String[0])); + } else { + completeSkuDetail(); + } + } + + public void addSkuDetail(String itemJson) { + JSONObject o = null; + try { + o = new JSONObject(itemJson); + Dictionary item = new Dictionary(); + item.put("type", o.optString("type")); + item.put("product_id", o.optString("productId")); + item.put("title", o.optString("title")); + item.put("description", o.optString("description")); + item.put("price", o.optString("price")); + item.put("price_currency_code", o.optString("price_currency_code")); + item.put("price_amount", 0.000001d * o.optLong("price_amount_micros")); + mSkuDetails.put(item.get("product_id").toString(), item); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public void completeSkuDetail() { + GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails }); + } + + public void errorSkuDetail(String errorMessage) { + GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage }); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java new file mode 100644 index 0000000000..8e3775c2a9 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java @@ -0,0 +1,61 @@ +/*************************************************************************/ +/* GodotRenderer.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.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import org.godotengine.godot.utils.GLUtils; + +/** + * Godot's renderer implementation. + */ +class GodotRenderer implements GLSurfaceView.Renderer { + + public void onDrawFrame(GL10 gl) { + GodotLib.step(); + for (int i = 0; i < Godot.singleton_count; i++) { + Godot.singletons[i].onGLDrawFrame(gl); + } + } + + public void onSurfaceChanged(GL10 gl, int width, int height) { + + GodotLib.resize(width, height); + for (int i = 0; i < Godot.singleton_count; i++) { + Godot.singletons[i].onGLSurfaceChanged(gl, width, height); + } + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + GodotLib.newcontext(GLUtils.use_32); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java new file mode 100644 index 0000000000..fc3e47e69d --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java @@ -0,0 +1,170 @@ +/*************************************************************************/ +/* GodotView.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.annotation.SuppressLint; +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.view.KeyEvent; +import android.view.MotionEvent; +import org.godotengine.godot.input.GodotInputHandler; +import org.godotengine.godot.utils.GLUtils; +import org.godotengine.godot.xr.XRMode; +import org.godotengine.godot.xr.ovr.OvrConfigChooser; +import org.godotengine.godot.xr.ovr.OvrContextFactory; +import org.godotengine.godot.xr.ovr.OvrWindowSurfaceFactory; +import org.godotengine.godot.xr.regular.RegularConfigChooser; +import org.godotengine.godot.xr.regular.RegularContextFactory; +import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; + +/** + * A simple GLSurfaceView sub-class that demonstrate how to perform + * OpenGL ES 2.0 rendering into a GL Surface. Note the following important + * details: + * + * - The class must use a custom context factory to enable 2.0 rendering. + * See ContextFactory class definition below. + * + * - The class must use a custom EGLConfigChooser to be able to select + * an EGLConfig that supports 2.0. This is done by providing a config + * specification to eglChooseConfig() that has the attribute + * EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag + * set. See ConfigChooser class definition below. + * + * - The class must select the surface's format, then choose an EGLConfig + * that matches it exactly (with regards to red/green/blue/alpha channels + * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. + */ +public class GodotView extends GLSurfaceView { + + private static String TAG = GodotView.class.getSimpleName(); + + private final Godot activity; + private final GodotInputHandler inputHandler; + + public GodotView(Godot activity, XRMode xrMode, boolean p_use_gl3, boolean p_use_32_bits, boolean p_use_debug_opengl) { + super(activity); + GLUtils.use_gl3 = p_use_gl3; + GLUtils.use_32 = p_use_32_bits; + GLUtils.use_debug_opengl = p_use_debug_opengl; + + this.activity = activity; + this.inputHandler = new GodotInputHandler(this); + init(xrMode, false, 16, 0); + } + + public void initInputDevices() { + this.inputHandler.initInputDevices(); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + return activity.gotTouchEvent(event); + } + + @Override + public boolean onKeyUp(final int keyCode, KeyEvent event) { + return inputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); + } + + @Override + public boolean onKeyDown(final int keyCode, KeyEvent event) { + return inputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + return inputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); + } + + private void init(XRMode xrMode, boolean translucent, int depth, int stencil) { + + setPreserveEGLContextOnPause(true); + setFocusableInTouchMode(true); + switch (xrMode) { + + case OVR: + // Replace the default egl config chooser. + setEGLConfigChooser(new OvrConfigChooser()); + + // Replace the default context factory. + setEGLContextFactory(new OvrContextFactory()); + + // Replace the default window surface factory. + setEGLWindowSurfaceFactory(new OvrWindowSurfaceFactory()); + break; + + case REGULAR: + default: + /* By default, GLSurfaceView() creates a RGB_565 opaque surface. + * If we want a translucent one, we should change the surface's + * format here, using PixelFormat.TRANSLUCENT for GL Surfaces + * is interpreted as any 32-bit surface with alpha by SurfaceFlinger. + */ + if (translucent) { + this.getHolder().setFormat(PixelFormat.TRANSLUCENT); + } + + /* Setup the context factory for 2.0 rendering. + * See ContextFactory class definition below + */ + setEGLContextFactory(new RegularContextFactory()); + + /* We need to choose an EGLConfig that matches the format of + * our surface exactly. This is going to be done in our + * custom config chooser. See ConfigChooser class definition + * below. + */ + + if (GLUtils.use_32) { + setEGLConfigChooser(translucent ? + new RegularFallbackConfigChooser(8, 8, 8, 8, 24, stencil, + new RegularConfigChooser(8, 8, 8, 8, 16, stencil)) : + new RegularFallbackConfigChooser(8, 8, 8, 8, 24, stencil, + new RegularConfigChooser(5, 6, 5, 0, 16, stencil))); + + } else { + setEGLConfigChooser(translucent ? + new RegularConfigChooser(8, 8, 8, 8, 16, stencil) : + new RegularConfigChooser(5, 6, 5, 0, 16, stencil)); + } + break; + } + + /* Set the renderer responsible for frame rendering */ + setRenderer(new GodotRenderer()); + } + + public void onBackPressed() { + activity.onBackPressed(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java new file mode 100644 index 0000000000..45b739baa0 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -0,0 +1,171 @@ +/*************************************************************************/ +/* GodotEditText.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.input; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import java.lang.ref.WeakReference; +import org.godotengine.godot.*; + +public class GodotEditText extends EditText { + // =========================================================== + // Constants + // =========================================================== + private final static int HANDLER_OPEN_IME_KEYBOARD = 2; + private final static int HANDLER_CLOSE_IME_KEYBOARD = 3; + + // =========================================================== + // Fields + // =========================================================== + private GodotView mView; + private GodotTextInputWrapper mInputWrapper; + private EditHandler sHandler = new EditHandler(this); + private String mOriginText; + + private static class EditHandler extends Handler { + private final WeakReference<GodotEditText> mEdit; + public EditHandler(GodotEditText edit) { + mEdit = new WeakReference<>(edit); + } + + @Override + public void handleMessage(Message msg) { + GodotEditText edit = mEdit.get(); + if (edit != null) { + edit.handleMessage(msg); + } + } + } + + // =========================================================== + // Constructors + // =========================================================== + public GodotEditText(final Context context) { + super(context); + this.initView(); + } + + public GodotEditText(final Context context, final AttributeSet attrs) { + super(context, attrs); + this.initView(); + } + + public GodotEditText(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + this.initView(); + } + + protected void initView() { + this.setPadding(0, 0, 0, 0); + this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + } + + private void handleMessage(final Message msg) { + switch (msg.what) { + case HANDLER_OPEN_IME_KEYBOARD: { + GodotEditText edit = (GodotEditText)msg.obj; + String text = edit.mOriginText; + if (edit.requestFocus()) { + edit.removeTextChangedListener(edit.mInputWrapper); + edit.setText(""); + edit.append(text); + edit.mInputWrapper.setOriginText(text); + edit.addTextChangedListener(edit.mInputWrapper); + final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(edit, 0); + } + } break; + + case HANDLER_CLOSE_IME_KEYBOARD: { + GodotEditText edit = (GodotEditText)msg.obj; + + edit.removeTextChangedListener(mInputWrapper); + final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); + edit.mView.requestFocus(); + } break; + } + } + + // =========================================================== + // Getter & Setter + // =========================================================== + public void setView(final GodotView view) { + this.mView = view; + if (mInputWrapper == null) + mInputWrapper = new GodotTextInputWrapper(mView, this); + this.setOnEditorActionListener(mInputWrapper); + view.requestFocus(); + } + + // =========================================================== + // Methods for/from SuperClass/Interfaces + // =========================================================== + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { + super.onKeyDown(keyCode, keyEvent); + + /* Let GlSurfaceView get focus if back key is input. */ + if (keyCode == KeyEvent.KEYCODE_BACK) { + this.mView.requestFocus(); + } + + return true; + } + + // =========================================================== + // Methods + // =========================================================== + public void showKeyboard(String p_existing_text) { + this.mOriginText = p_existing_text; + + final Message msg = new Message(); + msg.what = HANDLER_OPEN_IME_KEYBOARD; + msg.obj = this; + sHandler.sendMessage(msg); + } + + public void hideKeyboard() { + final Message msg = new Message(); + msg.what = HANDLER_CLOSE_IME_KEYBOARD; + msg.obj = this; + sHandler.sendMessage(msg); + } + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java new file mode 100644 index 0000000000..a443a0ad90 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -0,0 +1,360 @@ +/*************************************************************************/ +/* GodotInputHandler.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.input; + +import static org.godotengine.godot.utils.GLUtils.DEBUG; + +import android.util.Log; +import android.view.InputDevice; +import android.view.InputDevice.MotionRange; +import android.view.KeyEvent; +import android.view.MotionEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.godotengine.godot.GodotLib; +import org.godotengine.godot.GodotView; +import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; + +/** + * Handles input related events for the {@link GodotView} view. + */ +public class GodotInputHandler implements InputDeviceListener { + + private final ArrayList<Joystick> joysticksDevices = new ArrayList<Joystick>(); + + private final GodotView godotView; + private final InputManagerCompat inputManager; + + public GodotInputHandler(GodotView godotView) { + this.godotView = godotView; + this.inputManager = InputManagerCompat.Factory.getInputManager(godotView.getContext()); + this.inputManager.registerInputDeviceListener(this, null); + } + + private void queueEvent(Runnable task) { + godotView.queueEvent(task); + } + + private boolean isKeyEvent_GameDevice(int source) { + // Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD) + if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD)) + return false; + + return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; + } + + public boolean onKeyUp(final int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + return true; + } + + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + return false; + }; + + int source = event.getSource(); + if (isKeyEvent_GameDevice(source)) { + + final int button = getGodotButton(keyCode); + final int device_id = findJoystickDevice(event.getDeviceId()); + + // Check if the device exists + if (device_id > -1) { + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joybutton(device_id, button, false); + } + }); + return true; + } + } else { + final int chr = event.getUnicodeChar(0); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.key(keyCode, chr, false); + } + }); + }; + + return false; + } + + public boolean onKeyDown(final int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + godotView.onBackPressed(); + // press 'back' button should not terminate program + //normal handle 'back' event in game logic + return true; + } + + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + return false; + }; + + int source = event.getSource(); + //Log.e(TAG, String.format("Key down! source %d, device %d, joystick %d, %d, %d", event.getDeviceId(), source, (source & InputDevice.SOURCE_JOYSTICK), (source & InputDevice.SOURCE_DPAD), (source & InputDevice.SOURCE_GAMEPAD))); + + if (isKeyEvent_GameDevice(source)) { + + if (event.getRepeatCount() > 0) // ignore key echo + return true; + + final int button = getGodotButton(keyCode); + final int device_id = findJoystickDevice(event.getDeviceId()); + + // Check if the device exists + if (device_id > -1) { + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joybutton(device_id, button, true); + } + }); + return true; + } + } else { + final int chr = event.getUnicodeChar(0); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.key(keyCode, chr, true); + } + }); + }; + + return false; + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) { + + final int device_id = findJoystickDevice(event.getDeviceId()); + + // Check if the device exists + if (device_id > -1) { + Joystick joy = joysticksDevices.get(device_id); + + for (int i = 0; i < joy.axes.size(); i++) { + InputDevice.MotionRange range = joy.axes.get(i); + final float value = (event.getAxisValue(range.getAxis()) - range.getMin()) / range.getRange() * 2.0f - 1.0f; + final int idx = i; + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joyaxis(device_id, idx, value); + } + }); + } + + for (int i = 0; i < joy.hats.size(); i += 2) { + final int hatX = Math.round(event.getAxisValue(joy.hats.get(i).getAxis())); + final int hatY = Math.round(event.getAxisValue(joy.hats.get(i + 1).getAxis())); + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joyhat(device_id, hatX, hatY); + } + }); + } + return true; + } + }; + + return false; + } + + public void initInputDevices() { + /* initially add input devices*/ + int[] deviceIds = inputManager.getInputDeviceIds(); + for (int deviceId : deviceIds) { + InputDevice device = inputManager.getInputDevice(deviceId); + if (DEBUG) { + Log.v("GodotView", String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + } + onInputDeviceAdded(deviceId); + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + int id = findJoystickDevice(deviceId); + + // Check if the device has not been already added + if (id < 0) { + InputDevice device = inputManager.getInputDevice(deviceId); + //device can be null if deviceId is not found + if (device != null) { + int sources = device.getSources(); + if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || + ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) { + id = joysticksDevices.size(); + + Joystick joy = new Joystick(); + joy.device_id = deviceId; + joy.name = device.getName(); + joy.axes = new ArrayList<InputDevice.MotionRange>(); + joy.hats = new ArrayList<InputDevice.MotionRange>(); + + List<InputDevice.MotionRange> ranges = device.getMotionRanges(); + Collections.sort(ranges, new RangeComparator()); + + for (InputDevice.MotionRange range : ranges) { + if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { + joy.hats.add(range); + } else { + joy.axes.add(range); + } + } + + joysticksDevices.add(joy); + + final int device_id = id; + final String name = joy.name; + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joyconnectionchanged(device_id, true, name); + } + }); + } + } + } + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + final int device_id = findJoystickDevice(deviceId); + + // Check if the evice has not been already removed + if (device_id > -1) { + joysticksDevices.remove(device_id); + + queueEvent(new Runnable() { + @Override + public void run() { + GodotLib.joyconnectionchanged(device_id, false, ""); + } + }); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + onInputDeviceRemoved(deviceId); + onInputDeviceAdded(deviceId); + } + + private static class RangeComparator implements Comparator<MotionRange> { + @Override + public int compare(MotionRange arg0, MotionRange arg1) { + return arg0.getAxis() - arg1.getAxis(); + } + } + + public static int getGodotButton(int keyCode) { + int button; + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B + button = 0; + break; + case KeyEvent.KEYCODE_BUTTON_B: + button = 1; + break; + case KeyEvent.KEYCODE_BUTTON_X: // Android X is SNES Y + button = 2; + break; + case KeyEvent.KEYCODE_BUTTON_Y: + button = 3; + break; + case KeyEvent.KEYCODE_BUTTON_L1: + button = 9; + break; + case KeyEvent.KEYCODE_BUTTON_L2: + button = 15; + break; + case KeyEvent.KEYCODE_BUTTON_R1: + button = 10; + break; + case KeyEvent.KEYCODE_BUTTON_R2: + button = 16; + break; + case KeyEvent.KEYCODE_BUTTON_SELECT: + button = 4; + break; + case KeyEvent.KEYCODE_BUTTON_START: + button = 6; + break; + case KeyEvent.KEYCODE_BUTTON_THUMBL: + button = 7; + break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: + button = 8; + break; + case KeyEvent.KEYCODE_DPAD_UP: + button = 11; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + button = 12; + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + button = 13; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + button = 14; + break; + case KeyEvent.KEYCODE_BUTTON_C: + button = 17; + break; + case KeyEvent.KEYCODE_BUTTON_Z: + button = 18; + break; + + default: + button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20; + break; + } + return button; + } + + private int findJoystickDevice(int device_id) { + for (int i = 0; i < joysticksDevices.size(); i++) { + if (joysticksDevices.get(i).device_id == device_id) { + return i; + } + } + + return -1; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java new file mode 100644 index 0000000000..9b372c75e3 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -0,0 +1,150 @@ +/*************************************************************************/ +/* GodotTextInputWrapper.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.input; +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import org.godotengine.godot.*; + +public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListener { + // =========================================================== + // Constants + // =========================================================== + private static final String TAG = GodotTextInputWrapper.class.getSimpleName(); + + // =========================================================== + // Fields + // =========================================================== + private final GodotView mView; + private final GodotEditText mEdit; + private String mOriginText; + + // =========================================================== + // Constructors + // =========================================================== + + public GodotTextInputWrapper(final GodotView view, final GodotEditText edit) { + this.mView = view; + this.mEdit = edit; + } + + // =========================================================== + // Getter & Setter + // =========================================================== + + private boolean isFullScreenEdit() { + final TextView textField = this.mEdit; + final InputMethodManager imm = (InputMethodManager)textField.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isFullscreenMode(); + } + + public void setOriginText(final String originText) { + this.mOriginText = originText; + } + + // =========================================================== + // Methods for/from SuperClass/Interfaces + // =========================================================== + + @Override + public void afterTextChanged(final Editable s) { + } + + @Override + public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { + //Log.d(TAG, "beforeTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",after: " + after); + + mView.queueEvent(new Runnable() { + @Override + public void run() { + for (int i = 0; i < count; ++i) { + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, true); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, false); + } + } + }); + } + + @Override + public void onTextChanged(final CharSequence pCharSequence, final int start, final int before, final int count) { + //Log.d(TAG, "onTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",before: " + before); + + final int[] newChars = new int[count]; + for (int i = start; i < start + count; ++i) { + newChars[i - start] = pCharSequence.charAt(i); + } + mView.queueEvent(new Runnable() { + @Override + public void run() { + for (int i = 0; i < count; ++i) { + GodotLib.key(0, newChars[i], true); + GodotLib.key(0, newChars[i], false); + } + } + }); + } + + @Override + public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { + if (this.mEdit == pTextView && this.isFullScreenEdit()) { + final String characters = pKeyEvent.getCharacters(); + + mView.queueEvent(new Runnable() { + @Override + public void run() { + for (int i = 0; i < characters.length(); i++) { + final int ch = characters.codePointAt(i); + GodotLib.key(0, ch, true); + GodotLib.key(0, ch, false); + } + } + }); + } + + if (pActionID == EditorInfo.IME_ACTION_DONE) { + this.mView.requestFocus(); + } + return false; + } + + // =========================================================== + // Methods + // =========================================================== + + // =========================================================== + // Inner and Anonymous Classes + // =========================================================== +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java new file mode 100644 index 0000000000..4042c42e9d --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.input; + +import android.content.Context; +import android.os.Handler; +import android.view.InputDevice; +import android.view.MotionEvent; + +public interface InputManagerCompat { + /** + * Gets information about the input device with the specified id. + * + * @param id The device id + * @return The input device or null if not found + */ + public InputDevice getInputDevice(int id); + + /** + * Gets the ids of all input devices in the system. + * + * @return The input device ids. + */ + public int[] getInputDeviceIds(); + + /** + * Registers an input device listener to receive notifications about when + * input devices are added, removed or changed. + * + * @param listener The listener to register. + * @param handler The handler on which the listener should be invoked, or + * null if the listener should be invoked on the calling thread's + * looper. + */ + public void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener, + Handler handler); + + /** + * Unregisters an input device listener. + * + * @param listener The listener to unregister. + */ + public void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener); + + /* + * The following three calls are to simulate V16 behavior on pre-Jellybean + * devices. If you don't call them, your callback will never be called + * pre-API 16. + */ + + /** + * Pass the motion events to the InputManagerCompat. This is used to + * optimize for polling for controllers. If you do not pass these events in, + * polling will cause regular object creation. + * + * @param event the motion event from the app + */ + public void onGenericMotionEvent(MotionEvent event); + + /** + * Tell the V9 input manager that it should stop polling for disconnected + * devices. You can call this during onPause in your activity, although you + * might want to call it whenever your game is not active (or whenever you + * don't care about being notified of new input devices) + */ + public void onPause(); + + /** + * Tell the V9 input manager that it should start polling for disconnected + * devices. You can call this during onResume in your activity, although you + * might want to call it less often (only when the gameplay is actually + * active) + */ + public void onResume(); + + public interface InputDeviceListener { + /** + * Called whenever the input manager detects that a device has been + * added. This will only be called in the V9 version when a motion event + * is detected. + * + * @param deviceId The id of the input device that was added. + */ + void onInputDeviceAdded(int deviceId); + + /** + * Called whenever the properties of an input device have changed since + * they were last queried. This will not be called for the V9 version of + * the API. + * + * @param deviceId The id of the input device that changed. + */ + void onInputDeviceChanged(int deviceId); + + /** + * Called whenever the input manager detects that a device has been + * removed. For the V9 version, this can take some time depending on the + * poll rate. + * + * @param deviceId The id of the input device that was removed. + */ + void onInputDeviceRemoved(int deviceId); + } + + /** + * Use this to construct a compatible InputManager. + */ + public static class Factory { + + /** + * Constructs and returns a compatible InputManger + * + * @param context the Context that will be used to get the system + * service from + * @return a compatible implementation of InputManager + */ + public static InputManagerCompat getInputManager(Context context) { + return new InputManagerV16(context); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java new file mode 100644 index 0000000000..e4bafa7ff9 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.input; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Build; +import android.os.Handler; +import android.view.InputDevice; +import android.view.MotionEvent; +import java.util.HashMap; +import java.util.Map; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class InputManagerV16 implements InputManagerCompat { + + private final InputManager mInputManager; + private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners; + + public InputManagerV16(Context context) { + mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); + mListeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>(); + } + + @Override + public InputDevice getInputDevice(int id) { + return mInputManager.getInputDevice(id); + } + + @Override + public int[] getInputDeviceIds() { + return mInputManager.getInputDeviceIds(); + } + + static class V16InputDeviceListener implements InputManager.InputDeviceListener { + final InputManagerCompat.InputDeviceListener mIDL; + + public V16InputDeviceListener(InputDeviceListener idl) { + mIDL = idl; + } + + @Override + public void onInputDeviceAdded(int deviceId) { + mIDL.onInputDeviceAdded(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + mIDL.onInputDeviceChanged(deviceId); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + mIDL.onInputDeviceRemoved(deviceId); + } + } + + @Override + public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { + V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); + mInputManager.registerInputDeviceListener(v16Listener, handler); + mListeners.put(listener, v16Listener); + } + + @Override + public void unregisterInputDeviceListener(InputDeviceListener listener) { + V16InputDeviceListener curListener = mListeners.remove(listener); + if (null != curListener) { + mInputManager.unregisterInputDeviceListener(curListener); + } + } + + @Override + public void onGenericMotionEvent(MotionEvent event) { + // unused in V16 + } + + @Override + public void onPause() { + // unused in V16 + } + + @Override + public void onResume() { + // unused in V16 + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java b/platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java new file mode 100644 index 0000000000..ff95bfb0c5 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java @@ -0,0 +1,44 @@ +/*************************************************************************/ +/* Joystick.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.input; + +import android.view.InputDevice.MotionRange; +import java.util.ArrayList; + +/** + * POJO class to represent a Joystick input device. + */ +class Joystick { + int device_id; + String name; + ArrayList<MotionRange> axes; + ArrayList<MotionRange> hats; +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java new file mode 100644 index 0000000000..4c1050c948 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java @@ -0,0 +1,116 @@ +/*************************************************************************/ +/* ConsumeTask.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.payments; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.RemoteException; +import com.android.vending.billing.IInAppBillingService; +import java.lang.ref.WeakReference; + +abstract public class ConsumeTask { + + private Context context; + private IInAppBillingService mService; + + private String mSku; + private String mToken; + + private static class ConsumeAsyncTask extends AsyncTask<String, String, String> { + + private WeakReference<ConsumeTask> mTask; + + ConsumeAsyncTask(ConsumeTask consume) { + mTask = new WeakReference<>(consume); + } + + @Override + protected String doInBackground(String... strings) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(strings); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ConsumeTask consume = mTask.get(); + if (consume != null) { + consume.onPostExecute(param); + } + } + } + + public ConsumeTask(IInAppBillingService mService, Context context) { + this.context = context; + this.mService = mService; + } + + public void consume(final String sku) { + mSku = sku; + PaymentsCache pc = new PaymentsCache(context); + Boolean isBlocked = pc.getConsumableFlag("block", sku); + mToken = pc.getConsumableValue("token", sku); + if (!isBlocked && mToken == null) { + // Consuming task is processing + } else if (!isBlocked) { + return; + } else if (mToken == null) { + this.error("No token for sku:" + sku); + return; + } + new ConsumeAsyncTask(this).execute(); + } + + private String doInBackground(String... params) { + try { + int response = mService.consumePurchase(3, context.getPackageName(), mToken); + if (response == 0 || response == 8) { + return null; + } + } catch (RemoteException e) { + return e.getMessage(); + } + return "Some error"; + } + + private void onPostExecute(String param) { + if (param == null) { + success(new PaymentsCache(context).getConsumableValue("ticket", mSku)); + } else { + error(param); + } + } + + abstract protected void success(String ticket); + abstract protected void error(String message); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java new file mode 100644 index 0000000000..1a914967a2 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java @@ -0,0 +1,93 @@ +/*************************************************************************/ +/* HandlePurchaseTask.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.payments; + +import android.app.Activity; +import android.content.Intent; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class HandlePurchaseTask { + + private Activity context; + + public HandlePurchaseTask(Activity context) { + this.context = context; + } + + public void handlePurchaseRequest(int resultCode, Intent data) { + //Log.d("XXX", "Handling purchase response"); + if (resultCode == Activity.RESULT_OK) { + try { + //int responseCode = data.getIntExtra("RESPONSE_CODE", 0); + PaymentsCache pc = new PaymentsCache(context); + + String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); + //Log.d("XXX", "Purchase data:" + purchaseData); + String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); + //Log.d("XXX", "Purchase signature:" + dataSignature); + //Log.d("SARLANGA", purchaseData); + + JSONObject jo = new JSONObject(purchaseData); + //String sku = jo.getString("productId"); + //alert("You have bought the " + sku + ". Excellent choice, aventurer!"); + //String orderId = jo.getString("orderId"); + //String packageName = jo.getString("packageName"); + String productId = jo.getString("productId"); + //Long purchaseTime = jo.getLong("purchaseTime"); + //Integer state = jo.getInt("purchaseState"); + String developerPayload = jo.getString("developerPayload"); + String purchaseToken = jo.getString("purchaseToken"); + + if (!pc.getConsumableValue("validation_hash", productId).equals(developerPayload)) { + error("Untrusted callback"); + return; + } + //Log.d("XXX", "Este es el product ID:" + productId); + pc.setConsumableValue("ticket_signautre", productId, dataSignature); + pc.setConsumableValue("ticket", productId, purchaseData); + pc.setConsumableFlag("block", productId, true); + pc.setConsumableValue("token", productId, purchaseToken); + + success(productId, dataSignature, purchaseData); + return; + } catch (JSONException e) { + error(e.getMessage()); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + canceled(); + } + } + + abstract protected void success(String sku, String signature, String ticket); + abstract protected void error(String message); + abstract protected void canceled(); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java new file mode 100644 index 0000000000..8a2facbcfb --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java @@ -0,0 +1,72 @@ +/*************************************************************************/ +/* PaymentsCache.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.payments; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +public class PaymentsCache { + + public Context context; + + public PaymentsCache(Context context) { + this.context = context; + } + + public void setConsumableFlag(String set, String sku, Boolean flag) { + SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putBoolean(sku, flag); + editor.apply(); + } + + public boolean getConsumableFlag(String set, String sku) { + SharedPreferences sharedPref = context.getSharedPreferences( + "consumables_" + set, Context.MODE_PRIVATE); + return sharedPref.getBoolean(sku, false); + } + + public void setConsumableValue(String set, String sku, String value) { + SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(sku, value); + //Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku); + editor.apply(); + } + + public String getConsumableValue(String set, String sku) { + SharedPreferences sharedPref = context.getSharedPreferences( + "consumables_" + set, Context.MODE_PRIVATE); + //Log.d("XXX", "Getting asset: consumables_" + set + ":" + sku); + return sharedPref.getString(sku, null); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java new file mode 100644 index 0000000000..c079c55854 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java @@ -0,0 +1,419 @@ +/*************************************************************************/ +/* PaymentsManager.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.payments; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import com.android.vending.billing.IInAppBillingService; +import java.util.ArrayList; +import java.util.Arrays; +import org.godotengine.godot.GodotPaymentV3; +import org.json.JSONException; +import org.json.JSONObject; + +public class PaymentsManager { + + public static final int BILLING_RESPONSE_RESULT_OK = 0; + public static final int REQUEST_CODE_FOR_PURCHASE = 0x1001; + private static boolean auto_consume = true; + + private Activity activity; + IInAppBillingService mService; + + public void setActivity(Activity activity) { + this.activity = activity; + } + + public static PaymentsManager createManager(Activity activity) { + PaymentsManager manager = new PaymentsManager(activity); + return manager; + } + + private PaymentsManager(Activity activity) { + this.activity = activity; + } + + public PaymentsManager initService() { + Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + intent.setPackage("com.android.vending"); + activity.bindService( + intent, + mServiceConn, + Context.BIND_AUTO_CREATE); + return this; + } + + public void destroy() { + if (mService != null) { + activity.unbindService(mServiceConn); + } + } + + ServiceConnection mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + + // At this stage, godotPaymentV3 might not have been initialized yet. + if (godotPaymentV3 != null) { + godotPaymentV3.callbackDisconnected(); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IInAppBillingService.Stub.asInterface(service); + + // At this stage, godotPaymentV3 might not have been initialized yet. + if (godotPaymentV3 != null) { + godotPaymentV3.callbackConnected(); + } + } + }; + + public void requestPurchase(final String sku, String transactionId) { + new PurchaseTask(mService, activity) { + @Override + protected void error(String message) { + godotPaymentV3.callbackFail(message); + } + + @Override + protected void canceled() { + godotPaymentV3.callbackCancel(); + } + + @Override + protected void alreadyOwned() { + godotPaymentV3.callbackAlreadyOwned(sku); + } + } + .purchase(sku, transactionId); + } + + public boolean isConnected() { + return mService != null; + } + + public void consumeUnconsumedPurchases() { + new ReleaseAllConsumablesTask(mService, activity) { + @Override + protected void success(String sku, String receipt, String signature, String token) { + godotPaymentV3.callbackSuccessProductMassConsumed(receipt, signature, sku); + } + + @Override + protected void error(String message) { + Log.d("godot", "consumeUnconsumedPurchases :" + message); + godotPaymentV3.callbackFailConsume(message); + } + + @Override + protected void notRequired() { + Log.d("godot", "callbackSuccessNoUnconsumedPurchases :"); + godotPaymentV3.callbackSuccessNoUnconsumedPurchases(); + } + } + .consumeItAll(); + } + + public void requestPurchased() { + try { + PaymentsCache pc = new PaymentsCache(activity); + + String continueToken = null; + + do { + Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp", continueToken); + + if (bundle.getInt("RESPONSE_CODE") == 0) { + + final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); + + if (myPurchases == null || myPurchases.size() == 0) { + godotPaymentV3.callbackPurchased("", "", ""); + return; + } + + for (int i = 0; i < myPurchases.size(); i++) { + + try { + String receipt = myPurchases.get(i); + JSONObject inappPurchaseData = new JSONObject(receipt); + String sku = inappPurchaseData.getString("productId"); + String token = inappPurchaseData.getString("purchaseToken"); + String signature = mySignatures.get(i); + + pc.setConsumableValue("ticket_signautre", sku, signature); + pc.setConsumableValue("ticket", sku, receipt); + pc.setConsumableFlag("block", sku, true); + pc.setConsumableValue("token", sku, token); + + godotPaymentV3.callbackPurchased(receipt, signature, sku); + } catch (JSONException e) { + } + } + } + continueToken = bundle.getString("INAPP_CONTINUATION_TOKEN"); + Log.d("godot", "continue token = " + continueToken); + } while (!TextUtils.isEmpty(continueToken)); + } catch (Exception e) { + Log.d("godot", "Error requesting purchased products:" + e.getClass().getName() + ":" + e.getMessage()); + } + } + + public void processPurchaseResponse(int resultCode, Intent data) { + new HandlePurchaseTask(activity) { + @Override + protected void success(final String sku, final String signature, final String ticket) { + godotPaymentV3.callbackSuccess(ticket, signature, sku); + + if (auto_consume) { + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + } + + @Override + protected void error(String message) { + godotPaymentV3.callbackFail(message); + } + } + .consume(sku); + } + } + + @Override + protected void error(String message) { + godotPaymentV3.callbackFail(message); + } + + @Override + protected void canceled() { + godotPaymentV3.callbackCancel(); + } + } + .handlePurchaseRequest(resultCode, data); + } + + public void validatePurchase(String purchaseToken, final String sku) { + + new ValidateTask(activity, godotPaymentV3) { + @Override + protected void success() { + + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + godotPaymentV3.callbackSuccess(ticket, null, sku); + } + + @Override + protected void error(String message) { + godotPaymentV3.callbackFail(message); + } + } + .consume(sku); + } + + @Override + protected void error(String message) { + godotPaymentV3.callbackFail(message); + } + + @Override + protected void canceled() { + godotPaymentV3.callbackCancel(); + } + } + .validatePurchase(sku); + } + + public void setAutoConsume(boolean autoConsume) { + auto_consume = autoConsume; + } + + public void consume(final String sku) { + new ConsumeTask(mService, activity) { + @Override + protected void success(String ticket) { + godotPaymentV3.callbackSuccessProductMassConsumed(ticket, "", sku); + } + + @Override + protected void error(String message) { + godotPaymentV3.callbackFailConsume(message); + } + } + .consume(sku); + } + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromBundle(Bundle b) { + Object o = b.get("RESPONSE_CODE"); + if (o == null) { + //logDebug("Bundle with null response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } else if (o instanceof Integer) + return ((Integer)o).intValue(); + else if (o instanceof Long) + return (int)((Long)o).longValue(); + else { + //logError("Unexpected type for bundle response code."); + //logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); + } + } + + /** + * Returns a human-readable description for the given response code. + * + * @param code The response code + * @return A human-readable string explaining the result code. + * It also includes the result code numerically. + */ + public static String getResponseDesc(int code) { + String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + + + "3:Billing Unavailable/4:Item unavailable/" + + + "5:Developer Error/6:Error/7:Item Already Owned/" + + + "8:Item not owned") + .split("/"); + String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + + + "-1002:Bad response received/" + + + "-1003:Purchase signature verification failed/" + + + "-1004:Send intent failed/" + + + "-1005:User cancelled/" + + + "-1006:Unknown purchase response/" + + + "-1007:Missing token/" + + + "-1008:Unknown error/" + + + "-1009:Subscriptions not available/" + + + "-1010:Invalid consumption attempt") + .split("/"); + + if (code <= -1000) { + int index = -1000 - code; + if (index >= 0 && index < iabhelper_msgs.length) + return iabhelper_msgs[index]; + else + return String.valueOf(code) + ":Unknown IAB Helper Error"; + } else if (code < 0 || code >= iab_msgs.length) + return String.valueOf(code) + ":Unknown"; + else + return iab_msgs[code]; + } + + public void querySkuDetails(final String[] list) { + (new Thread(new Runnable() { + @Override + public void run() { + ArrayList<String> skuList = new ArrayList<String>(Arrays.asList(list)); + if (skuList.size() == 0) { + return; + } + // Split the sku list in blocks of no more than 20 elements. + ArrayList<ArrayList<String>> packs = new ArrayList<ArrayList<String>>(); + ArrayList<String> tempList; + int n = skuList.size() / 20; + int mod = skuList.size() % 20; + for (int i = 0; i < n; i++) { + tempList = new ArrayList<String>(); + for (String s : skuList.subList(i * 20, i * 20 + 20)) { + tempList.add(s); + } + packs.add(tempList); + } + if (mod != 0) { + tempList = new ArrayList<String>(); + for (String s : skuList.subList(n * 20, n * 20 + mod)) { + tempList.add(s); + } + packs.add(tempList); + } + for (ArrayList<String> skuPartList : packs) { + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList("ITEM_ID_LIST", skuPartList); + Bundle skuDetails = null; + try { + skuDetails = mService.getSkuDetails(3, activity.getPackageName(), "inapp", querySkus); + if (!skuDetails.containsKey("DETAILS_LIST")) { + int response = getResponseCodeFromBundle(skuDetails); + if (response != BILLING_RESPONSE_RESULT_OK) { + godotPaymentV3.errorSkuDetail(getResponseDesc(response)); + } else { + godotPaymentV3.errorSkuDetail("No error but no detail list."); + } + return; + } + + ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST"); + + for (String thisResponse : responseList) { + Log.d("godot", "response = " + thisResponse); + godotPaymentV3.addSkuDetail(thisResponse); + } + } catch (RemoteException e) { + e.printStackTrace(); + godotPaymentV3.errorSkuDetail("RemoteException error!"); + } + } + godotPaymentV3.completeSkuDetail(); + } + })) + .start(); + } + + private GodotPaymentV3 godotPaymentV3; + + public void setBaseSingleton(GodotPaymentV3 godotPaymentV3) { + this.godotPaymentV3 = godotPaymentV3; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java new file mode 100644 index 0000000000..9adc85e521 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java @@ -0,0 +1,118 @@ +/*************************************************************************/ +/* PurchaseTask.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.payments; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import com.android.vending.billing.IInAppBillingService; + +abstract public class PurchaseTask { + + private Activity context; + + private IInAppBillingService mService; + public PurchaseTask(IInAppBillingService mService, Activity context) { + this.context = context; + this.mService = mService; + } + + private boolean isLooping = false; + + public void purchase(final String sku, final String transactionId) { + Log.d("XXX", "Starting purchase for: " + sku); + PaymentsCache pc = new PaymentsCache(context); + Boolean isBlocked = pc.getConsumableFlag("block", sku); + /* + if(isBlocked){ + Log.d("XXX", "Is awaiting payment confirmation"); + error("Awaiting payment confirmation"); + return; + } + */ + final String hash = transactionId; + + Bundle buyIntentBundle; + try { + buyIntentBundle = mService.getBuyIntent(3, context.getApplicationContext().getPackageName(), sku, "inapp", hash); + } catch (RemoteException e) { + //Log.d("XXX", "Error: " + e.getMessage()); + error(e.getMessage()); + return; + } + Object rc = buyIntentBundle.get("RESPONSE_CODE"); + int responseCode = 0; + if (rc == null) { + responseCode = PaymentsManager.BILLING_RESPONSE_RESULT_OK; + } else if (rc instanceof Integer) { + responseCode = ((Integer)rc).intValue(); + } else if (rc instanceof Long) { + responseCode = (int)((Long)rc).longValue(); + } + //Log.d("XXX", "Buy intent response code: " + responseCode); + if (responseCode == 1 || responseCode == 3 || responseCode == 4) { + canceled(); + return; + } + if (responseCode == 7) { + alreadyOwned(); + return; + } + + PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT"); + pc.setConsumableValue("validation_hash", sku, hash); + try { + if (context == null) { + //Log.d("XXX", "No context!"); + } + if (pendingIntent == null) { + //Log.d("XXX", "No pending intent"); + } + //Log.d("XXX", "Starting activity for purchase!"); + context.startIntentSenderForResult( + pendingIntent.getIntentSender(), + PaymentsManager.REQUEST_CODE_FOR_PURCHASE, + new Intent(), + Integer.valueOf(0), Integer.valueOf(0), + Integer.valueOf(0)); + } catch (SendIntentException e) { + error(e.getMessage()); + } + } + + abstract protected void error(String message); + abstract protected void canceled(); + abstract protected void alreadyOwned(); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java new file mode 100644 index 0000000000..daca6ef5ae --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java @@ -0,0 +1,141 @@ +/*************************************************************************/ +/* ReleaseAllConsumablesTask.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.payments; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import com.android.vending.billing.IInAppBillingService; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class ReleaseAllConsumablesTask { + + private Context context; + private IInAppBillingService mService; + + private static class ReleaseAllConsumablesAsyncTask extends AsyncTask<String, String, String> { + + private WeakReference<ReleaseAllConsumablesTask> mTask; + private String mSku; + private String mReceipt; + private String mSignature; + private String mToken; + + ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) { + mTask = new WeakReference<ReleaseAllConsumablesTask>(task); + + mSku = sku; + mReceipt = receipt; + mSignature = signature; + mToken = token; + } + + @Override + protected String doInBackground(String... params) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + return consume.doInBackground(mToken); + } + return null; + } + + @Override + protected void onPostExecute(String param) { + ReleaseAllConsumablesTask consume = mTask.get(); + if (consume != null) { + consume.success(mSku, mReceipt, mSignature, mToken); + } + } + } + + public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) { + this.context = context; + this.mService = mService; + } + + public void consumeItAll() { + try { + //Log.d("godot", "consumeItall for " + context.getPackageName()); + Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null); + + if (bundle.getInt("RESPONSE_CODE") == 0) { + + final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); + final ArrayList<String> mySignatures = bundle.getStringArrayList("INAPP_DATA_SIGNATURE_LIST"); + + if (myPurchases == null || myPurchases.size() == 0) { + //Log.d("godot", "No purchases!"); + notRequired(); + return; + } + + //Log.d("godot", "# products to be consumed:" + myPurchases.size()); + for (int i = 0; i < myPurchases.size(); i++) { + + try { + String receipt = myPurchases.get(i); + JSONObject inappPurchaseData = new JSONObject(receipt); + String sku = inappPurchaseData.getString("productId"); + String token = inappPurchaseData.getString("purchaseToken"); + String signature = mySignatures.get(i); + //Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt); + new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute(); + } catch (JSONException e) { + } + } + } + } catch (Exception e) { + Log.d("godot", "Error releasing products:" + e.getClass().getName() + ":" + e.getMessage()); + } + } + + private String doInBackground(String token) { + try { + //Log.d("godot", "Requesting to consume an item with token ." + token); + int response = mService.consumePurchase(3, context.getPackageName(), token); + //Log.d("godot", "consumePurchase response: " + response); + if (response == 0 || response == 8) { + return null; + } + } catch (Exception e) { + Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); + } + return null; + } + + abstract protected void success(String sku, String receipt, String signature, String token); + abstract protected void error(String message); + abstract protected void notRequired(); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java new file mode 100644 index 0000000000..17a2a197ad --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java @@ -0,0 +1,142 @@ +/*************************************************************************/ +/* ValidateTask.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.payments; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import java.lang.ref.WeakReference; +import org.godotengine.godot.GodotPaymentV3; +import org.godotengine.godot.utils.HttpRequester; +import org.godotengine.godot.utils.RequestParams; +import org.json.JSONException; +import org.json.JSONObject; + +abstract public class ValidateTask { + + private Activity context; + private GodotPaymentV3 godotPaymentsV3; + private ProgressDialog dialog; + private String mSku; + + private static class ValidateAsyncTask extends AsyncTask<String, String, String> { + private WeakReference<ValidateTask> mTask; + + ValidateAsyncTask(ValidateTask task) { + mTask = new WeakReference<>(task); + } + + @Override + protected void onPreExecute() { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPreExecute(); + } + } + + @Override + protected String doInBackground(String... params) { + ValidateTask task = mTask.get(); + if (task != null) { + return task.doInBackground(params); + } + return null; + } + + @Override + protected void onPostExecute(String response) { + ValidateTask task = mTask.get(); + if (task != null) { + task.onPostExecute(response); + } + } + } + + public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) { + this.context = context; + this.godotPaymentsV3 = godotPaymentsV3; + } + + public void validatePurchase(final String sku) { + mSku = sku; + new ValidateAsyncTask(this).execute(); + } + + private void onPreExecute() { + dialog = ProgressDialog.show(context, null, "Please wait..."); + } + + private String doInBackground(String... params) { + PaymentsCache pc = new PaymentsCache(context); + String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); + RequestParams param = new RequestParams(); + param.setUrl(url); + param.put("ticket", pc.getConsumableValue("ticket", mSku)); + param.put("purchaseToken", pc.getConsumableValue("token", mSku)); + param.put("sku", mSku); + //Log.d("XXX", "Haciendo request a " + url); + //Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); + //Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); + //Log.d("XXX", "sku: " + sku); + param.put("package", context.getApplicationContext().getPackageName()); + HttpRequester requester = new HttpRequester(); + String jsonResponse = requester.post(param); + //Log.d("XXX", "Validation response:\n"+jsonResponse); + return jsonResponse; + } + + private void onPostExecute(String response) { + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + JSONObject j; + try { + j = new JSONObject(response); + if (j.getString("status").equals("OK")) { + success(); + return; + } else if (j.getString("status") != null) { + error(j.getString("message")); + } else { + error("Connection error"); + } + } catch (JSONException e) { + error(e.getMessage()); + } catch (Exception e) { + error(e.getMessage()); + } + } + + abstract protected void success(); + abstract protected void error(String message); + abstract protected void canceled(); +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java new file mode 100644 index 0000000000..4c551d1d21 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java @@ -0,0 +1,69 @@ +/*************************************************************************/ +/* Crypt.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.utils; + +import java.security.MessageDigest; +import java.util.Random; + +public class Crypt { + + public static String md5(String input) { + try { + // Create MD5 Hash + MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); + digest.update(input.getBytes()); + byte messageDigest[] = digest.digest(); + + // Create Hex String + StringBuffer hexString = new StringBuffer(); + for (int i = 0; i < messageDigest.length; i++) + hexString.append(Integer.toHexString(0xFF & messageDigest[i])); + return hexString.toString(); + + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + public static String createRandomHash() { + return md5(Long.toString(createRandomLong())); + } + + public static long createAbsRandomLong() { + return Math.abs(createRandomLong()); + } + + public static long createRandomLong() { + Random r = new Random(); + return r.nextLong(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java b/platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java new file mode 100644 index 0000000000..b61007faa3 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java @@ -0,0 +1,69 @@ +/*************************************************************************/ +/* CustomSSLSocketFactory.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.utils; +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import org.apache.http.conn.ssl.SSLSocketFactory; + +/** + * + * @author Luis Linietsky <luis.linietsky@gmail.com> + */ +public class CustomSSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public CustomSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(truststore); + + sslContext.init(null, tmf.getTrustManagers(), null); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException { + return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java new file mode 100644 index 0000000000..6c95494f8b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java @@ -0,0 +1,157 @@ +/*************************************************************************/ +/* GLUtils.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.utils; + +import android.util.Log; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +/** + * Contains GL utilities methods. + */ +public class GLUtils { + + private static final String TAG = GLUtils.class.getSimpleName(); + + public static final boolean DEBUG = false; + + public static boolean use_gl3 = false; + public static boolean use_32 = false; + public static boolean use_debug_opengl = false; + + private static final String[] ATTRIBUTES_NAMES = new String[] { + "EGL_BUFFER_SIZE", + "EGL_ALPHA_SIZE", + "EGL_BLUE_SIZE", + "EGL_GREEN_SIZE", + "EGL_RED_SIZE", + "EGL_DEPTH_SIZE", + "EGL_STENCIL_SIZE", + "EGL_CONFIG_CAVEAT", + "EGL_CONFIG_ID", + "EGL_LEVEL", + "EGL_MAX_PBUFFER_HEIGHT", + "EGL_MAX_PBUFFER_PIXELS", + "EGL_MAX_PBUFFER_WIDTH", + "EGL_NATIVE_RENDERABLE", + "EGL_NATIVE_VISUAL_ID", + "EGL_NATIVE_VISUAL_TYPE", + "EGL_PRESERVED_RESOURCES", + "EGL_SAMPLES", + "EGL_SAMPLE_BUFFERS", + "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", + "EGL_TRANSPARENT_RED_VALUE", + "EGL_TRANSPARENT_GREEN_VALUE", + "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", + "EGL_BIND_TO_TEXTURE_RGBA", + "EGL_MIN_SWAP_INTERVAL", + "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", + "EGL_ALPHA_MASK_SIZE", + "EGL_COLOR_BUFFER_TYPE", + "EGL_RENDERABLE_TYPE", + "EGL_CONFORMANT" + }; + + private static final int[] ATTRIBUTES = new int[] { + EGL10.EGL_BUFFER_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_RED_SIZE, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_HEIGHT, + EGL10.EGL_MAX_PBUFFER_PIXELS, + EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, + EGL10.EGL_NATIVE_VISUAL_ID, + EGL10.EGL_NATIVE_VISUAL_TYPE, + 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, + EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, + EGL10.EGL_TRANSPARENT_GREEN_VALUE, + EGL10.EGL_TRANSPARENT_BLUE_VALUE, + 0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB, + 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL, + 0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL, + EGL10.EGL_LUMINANCE_SIZE, + EGL10.EGL_ALPHA_MASK_SIZE, + EGL10.EGL_COLOR_BUFFER_TYPE, + EGL10.EGL_RENDERABLE_TYPE, + 0x3042 // EGL10.EGL_CONFORMANT + }; + + private GLUtils() {} + + public static void checkEglError(String tag, String prompt, EGL10 egl) { + int error; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + Log.e(tag, String.format("%s: EGL error: 0x%x", prompt, error)); + } + } + + public static void printConfigs(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + int numConfigs = configs.length; + Log.v(TAG, String.format("%d configurations", numConfigs)); + for (int i = 0; i < numConfigs; i++) { + Log.v(TAG, String.format("Configuration %d:\n", i)); + printConfig(egl, display, configs[i]); + } + } + + private static void printConfig(EGL10 egl, EGLDisplay display, + EGLConfig config) { + int[] value = new int[1]; + for (int i = 0; i < ATTRIBUTES.length; i++) { + int attribute = ATTRIBUTES[i]; + String name = ATTRIBUTES_NAMES[i]; + if (egl.eglGetConfigAttrib(display, config, attribute, value)) { + Log.i(TAG, String.format(" %s: %d\n", name, value[0])); + } else { + // Log.w(TAG, String.format(" %s: failed\n", name)); + while (egl.eglGetError() != EGL10.EGL_SUCCESS) + ; + } + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java b/platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java new file mode 100644 index 0000000000..02ae753b3e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java @@ -0,0 +1,227 @@ +/*************************************************************************/ +/* HttpRequester.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.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.security.KeyStore; +import java.util.Date; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.EntityUtils; + +/** + * + * @author Luis Linietsky <luis.linietsky@gmail.com> + */ +public class HttpRequester { + + private Context context; + private static final int TTL = 600000; // 10 minutos + private long cttl = 0; + + public HttpRequester() { + //Log.d("XXX", "Creando http request sin contexto"); + } + + public HttpRequester(Context context) { + this.context = context; + //Log.d("XXX", "Creando http request con contexto"); + } + + public String post(RequestParams params) { + HttpPost httppost = new HttpPost(params.getUrl()); + try { + httppost.setEntity(new UrlEncodedFormEntity(params.toPairsList())); + return request(httppost); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + public String get(RequestParams params) { + String response = getResponseFromCache(params.getUrl()); + if (response == null) { + //Log.d("XXX", "Cache miss!"); + HttpGet httpget = new HttpGet(params.getUrl()); + long timeInit = new Date().getTime(); + response = request(httpget); + long delay = new Date().getTime() - timeInit; + Log.d("HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds"); + if (response == null || response.length() == 0) { + response = ""; + } else { + saveResponseIntoCache(params.getUrl(), response); + } + } + Log.d("XXX", "Req: " + params.getUrl()); + Log.d("XXX", "Resp: " + response); + return response; + } + + private String request(HttpUriRequest request) { + //Log.d("XXX", "Haciendo request a: " + request.getURI() ); + Log.d("PPP", "Haciendo request a: " + request.getURI()); + long init = new Date().getTime(); + HttpClient httpclient = getNewHttpClient(); + HttpParams httpParameters = httpclient.getParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, 0); + HttpConnectionParams.setSoTimeout(httpParameters, 0); + HttpConnectionParams.setTcpNoDelay(httpParameters, true); + try { + HttpResponse response = httpclient.execute(request); + Log.d("PPP", "Fin de request (" + (new Date().getTime() - init) + ") a: " + request.getURI()); + //Log.d("XXX1", "Status:" + response.getStatusLine().toString()); + if (response.getStatusLine().getStatusCode() == 200) { + String strResponse = EntityUtils.toString(response.getEntity()); + //Log.d("XXX2", strResponse); + return strResponse; + } else { + Log.d("XXX3", "Response status code:" + response.getStatusLine().getStatusCode() + "\n" + EntityUtils.toString(response.getEntity())); + return null; + } + + } catch (ClientProtocolException e) { + Log.d("XXX3", e.getMessage()); + } catch (IOException e) { + Log.d("XXX4", e.getMessage()); + } + return null; + } + + private HttpClient getNewHttpClient() { + try { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + + SSLSocketFactory sf = new CustomSSLSocketFactory(trustStore); + sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + + HttpParams params = new BasicHttpParams(); + HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); + HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + registry.register(new Scheme("https", sf, 443)); + + ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry); + + return new DefaultHttpClient(ccm, params); + } catch (Exception e) { + return new DefaultHttpClient(); + } + } + + private static String convertStreamToString(InputStream is) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + try { + while ((line = reader.readLine()) != null) { + sb.append((line + "\n")); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return sb.toString(); + } + + public void saveResponseIntoCache(String request, String response) { + if (context == null) { + //Log.d("XXX", "No context, cache failed!"); + return; + } + SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString("request_" + Crypt.md5(request), response); + editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl()); + editor.apply(); + } + + public String getResponseFromCache(String request) { + if (context == null) { + Log.d("XXX", "No context, cache miss"); + return null; + } + SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE); + long ttl = getResponseTtl(request); + if (ttl == 0l || (new Date().getTime() - ttl) > 0l) { + Log.d("XXX", "Cache invalid ttl:" + ttl + " vs now:" + new Date().getTime()); + return null; + } + return sharedPref.getString("request_" + Crypt.md5(request), null); + } + + public long getResponseTtl(String request) { + SharedPreferences sharedPref = context.getSharedPreferences( + "http_get_cache", Context.MODE_PRIVATE); + return sharedPref.getLong("request_" + Crypt.md5(request) + "_ttl", 0l); + } + + public long getTtl() { + return cttl > 0 ? cttl : TTL; + } + + public void setTtl(long ttl) { + this.cttl = (ttl * 1000) + new Date().getTime(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java b/platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java new file mode 100644 index 0000000000..b9fe0dd0c9 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java @@ -0,0 +1,85 @@ +/*************************************************************************/ +/* RequestParams.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.utils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +/** + * + * @author Luis Linietsky <luis.linietsky@gmail.com> + */ +public class RequestParams { + + private HashMap<String, String> params; + private String url; + + public RequestParams() { + params = new HashMap<String, String>(); + } + + public void put(String key, String value) { + params.put(key, value); + } + + public String get(String key) { + return params.get(key); + } + + public void remove(Object key) { + params.remove(key); + } + + public boolean has(String key) { + return params.containsKey(key); + } + + public List<NameValuePair> toPairsList() { + List<NameValuePair> fields = new ArrayList<NameValuePair>(); + + for (String key : params.keySet()) { + fields.add(new BasicNameValuePair(key, this.get(key))); + } + return fields; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java b/platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java new file mode 100644 index 0000000000..5896b23ac3 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java @@ -0,0 +1,51 @@ +/*************************************************************************/ +/* XRMode.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.xr; + +/** + * Godot available XR modes. + */ +public enum XRMode { + 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, String inputFallbackMapping) { + this.index = index; + this.label = label; + this.cmdLineArg = cmdLineArg; + this.inputFallbackMapping = inputFallbackMapping; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java new file mode 100644 index 0000000000..ff836a31ca --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java @@ -0,0 +1,112 @@ +/*************************************************************************/ +/* OvrConfigChooser.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.xr.ovr; + +import android.opengl.EGLExt; +import android.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; + +/** + * EGL config chooser for the Oculus Mobile VR SDK. + */ +public class OvrConfigChooser implements GLSurfaceView.EGLConfigChooser { + + private static final int[] CONFIG_ATTRIBS = { + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, // Need alpha for the multi-pass timewarp compositor + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL10.EGL_SAMPLES, 0, + EGL10.EGL_NONE + }; + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + // Do NOT use eglChooseConfig, because the Android EGL code pushes in + // multisample flags in eglChooseConfig if the user has selected the "force 4x + // MSAA" option in settings, and that is completely wasted for our warp + // target. + int[] numConfig = new int[1]; + if (!egl.eglGetConfigs(display, null, 0, numConfig)) { + throw new IllegalArgumentException("eglGetConfigs failed."); + } + + int configsCount = numConfig[0]; + if (configsCount <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[configsCount]; + if (!egl.eglGetConfigs(display, configs, configsCount, numConfig)) { + throw new IllegalArgumentException("eglGetConfigs #2 failed."); + } + + int[] value = new int[1]; + for (EGLConfig config : configs) { + egl.eglGetConfigAttrib(display, config, EGL10.EGL_RENDERABLE_TYPE, value); + if ((value[0] & EGLExt.EGL_OPENGL_ES3_BIT_KHR) != EGLExt.EGL_OPENGL_ES3_BIT_KHR) { + continue; + } + + // The pbuffer config also needs to be compatible with normal window rendering + // so it can share textures with the window context. + egl.eglGetConfigAttrib(display, config, EGL10.EGL_SURFACE_TYPE, value); + if ((value[0] & (EGL10.EGL_WINDOW_BIT | EGL10.EGL_PBUFFER_BIT)) != (EGL10.EGL_WINDOW_BIT | EGL10.EGL_PBUFFER_BIT)) { + continue; + } + + // Check each attribute in CONFIG_ATTRIBS (which are the attributes we care about) + // and ensure the value in config matches. + int attribIndex = 0; + while (CONFIG_ATTRIBS[attribIndex] != EGL10.EGL_NONE) { + egl.eglGetConfigAttrib(display, config, CONFIG_ATTRIBS[attribIndex], value); + if (value[0] != CONFIG_ATTRIBS[attribIndex + 1]) { + // Attribute key's value does not match the configs value. + // Start checking next config. + break; + } + + // Step by two because CONFIG_ATTRIBS is in key/value pairs. + attribIndex += 2; + } + + if (CONFIG_ATTRIBS[attribIndex] == EGL10.EGL_NONE) { + // All relevant attributes match, set the config and stop checking the rest. + return config; + } + } + return null; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java new file mode 100644 index 0000000000..5f6da8c672 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* OvrContextFactory.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.xr.ovr; + +import android.opengl.EGL14; +import android.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + +/** + * EGL Context factory for the Oculus mobile VR SDK. + */ +public class OvrContextFactory implements GLSurfaceView.EGLContextFactory { + + private static final int[] CONTEXT_ATTRIBS = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE + }; + + @Override + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + return egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, CONTEXT_ATTRIBS); + } + + @Override + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java new file mode 100644 index 0000000000..f1e38c35d8 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java @@ -0,0 +1,60 @@ +/*************************************************************************/ +/* OvrWindowSurfaceFactory.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.xr.ovr; + +import android.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +/** + * EGL window surface factory for the Oculus mobile VR SDK. + */ +public class OvrWindowSurfaceFactory implements GLSurfaceView.EGLWindowSurfaceFactory { + + private final static int[] SURFACE_ATTRIBS = { + EGL10.EGL_WIDTH, 16, + EGL10.EGL_HEIGHT, 16, + EGL10.EGL_NONE + }; + + @Override + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow) { + return egl.eglCreatePbufferSurface(display, config, SURFACE_ATTRIBS); + } + + @Override + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java new file mode 100644 index 0000000000..3836967f86 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java @@ -0,0 +1,151 @@ +/*************************************************************************/ +/* RegularConfigChooser.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.xr.regular; + +import android.opengl.GLSurfaceView; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import org.godotengine.godot.utils.GLUtils; + +/** + * Used to select the egl config for pancake games. + */ +public class RegularConfigChooser implements GLSurfaceView.EGLConfigChooser { + + private static final String TAG = RegularConfigChooser.class.getSimpleName(); + + private int[] mValue = new int[1]; + + /* This EGL config specification is used to specify 2.0 rendering. + * We use a minimum size of 4 bits for red/green/blue, but will + * perform actual matching in chooseConfig() below. + */ + private static int EGL_OPENGL_ES2_BIT = 4; + private static int[] s_configAttribs2 = { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + // EGL10.EGL_DEPTH_SIZE, 16, + // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + private static int[] s_configAttribs3 = { + EGL10.EGL_RED_SIZE, 4, + EGL10.EGL_GREEN_SIZE, 4, + EGL10.EGL_BLUE_SIZE, 4, + // EGL10.EGL_DEPTH_SIZE, 16, + // EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT + EGL10.EGL_NONE + }; + + public RegularConfigChooser(int r, int g, int b, int a, int depth, int stencil) { + mRedSize = r; + mGreenSize = g; + mBlueSize = b; + mAlphaSize = a; + mDepthSize = depth; + mStencilSize = stencil; + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + + /* Get the number of minimally matching EGL configurations + */ + int[] num_config = new int[1]; + egl.eglChooseConfig(display, GLUtils.use_gl3 ? s_configAttribs3 : s_configAttribs2, null, 0, num_config); + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + /* Allocate then read the array of minimally matching EGL configs + */ + EGLConfig[] configs = new EGLConfig[numConfigs]; + egl.eglChooseConfig(display, GLUtils.use_gl3 ? s_configAttribs3 : s_configAttribs2, configs, numConfigs, num_config); + + if (GLUtils.DEBUG) { + GLUtils.printConfigs(egl, display, configs); + } + /* Now return the "best" one + */ + return chooseConfig(egl, display, configs); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + + // We need at least mDepthSize and mStencilSize bits + if (d < mDepthSize || s < mStencilSize) + continue; + + // We want an *exact* match for red/green/blue/alpha + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + + if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize) + return config; + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java new file mode 100644 index 0000000000..4f1e9a696b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -0,0 +1,81 @@ +/*************************************************************************/ +/* RegularContextFactory.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.xr.regular; + +import android.opengl.GLSurfaceView; +import android.util.Log; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import org.godotengine.godot.GodotLib; +import org.godotengine.godot.utils.GLUtils; + +/** + * Factory used to setup the opengl context for pancake games. + */ +public class RegularContextFactory implements GLSurfaceView.EGLContextFactory { + private static final String TAG = RegularContextFactory.class.getSimpleName(); + + private static final int _EGL_CONTEXT_FLAGS_KHR = 0x30FC; + private static final int _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR = 0x00000001; + + private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + String driver_name = GodotLib.getGlobal("rendering/quality/driver/driver_name"); + if (GLUtils.use_gl3 && !driver_name.equals("GLES3")) { + GLUtils.use_gl3 = false; + } + if (GLUtils.use_gl3) + Log.w(TAG, "creating OpenGL ES 3.0 context :"); + else + Log.w(TAG, "creating OpenGL ES 2.0 context :"); + + GLUtils.checkEglError(TAG, "Before eglCreateContext", egl); + EGLContext context; + if (GLUtils.use_debug_opengl) { + int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; + int[] attrib_list3 = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, GLUtils.use_gl3 ? attrib_list3 : attrib_list2); + } else { + int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + int[] attrib_list3 = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, GLUtils.use_gl3 ? attrib_list3 : attrib_list2); + } + GLUtils.checkEglError(TAG, "After eglCreateContext", egl); + return context; + } + + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java new file mode 100644 index 0000000000..f5718ef2b3 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java @@ -0,0 +1,61 @@ +/*************************************************************************/ +/* RegularFallbackConfigChooser.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.xr.regular; + +import android.util.Log; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import org.godotengine.godot.utils.GLUtils; + +/* Fallback if 32bit View is not supported*/ +public class RegularFallbackConfigChooser extends RegularConfigChooser { + + private static final String TAG = RegularFallbackConfigChooser.class.getSimpleName(); + + private RegularConfigChooser fallback; + + public RegularFallbackConfigChooser(int r, int g, int b, int a, int depth, int stencil, RegularConfigChooser fallback) { + super(r, g, b, a, depth, stencil); + this.fallback = fallback; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + EGLConfig ec = super.chooseConfig(egl, display, configs); + if (ec == null) { + Log.w(TAG, "Trying ConfigChooser fallback"); + ec = fallback.chooseConfig(egl, display, configs); + GLUtils.use_32 = false; + } + return ec; + } +} |