summaryrefslogtreecommitdiff
path: root/platform/android/java/lib
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/java/lib')
-rw-r--r--platform/android/java/lib/AndroidManifest.xml19
-rw-r--r--platform/android/java/lib/CMakeLists.txt18
-rw-r--r--platform/android/java/lib/THIRDPARTY.md39
-rw-r--r--platform/android/java/lib/aidl/com/android/vending/billing/IInAppBillingService.aidl281
-rw-r--r--platform/android/java/lib/aidl/com/android/vending/licensing/ILicenseResultListener.aidl21
-rw-r--r--platform/android/java/lib/aidl/com/android/vending/licensing/ILicensingService.aidl23
-rw-r--r--platform/android/java/lib/build.gradle91
-rw-r--r--platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch300
-rw-r--r--platform/android/java/lib/patches/com.google.android.vending.licensing.patch42
-rw-r--r--platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.pngbin0 -> 1843 bytes
-rw-r--r--platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.pngbin0 -> 718 bytes
-rw-r--r--platform/android/java/lib/res/drawable-nodpi/icon.pngbin0 -> 7569 bytes
-rw-r--r--platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.pngbin0 -> 462 bytes
-rw-r--r--platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.pngbin0 -> 2830 bytes
-rw-r--r--platform/android/java/lib/res/layout/downloading_expansion.xml165
-rw-r--r--platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml108
-rw-r--r--platform/android/java/lib/res/values-ar/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-bg/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-ca/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-cs/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-da/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-de/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-el/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-en/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-es-rES/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-es/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-fa/strings.xml15
-rw-r--r--platform/android/java/lib/res/values-fi/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-fr/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-hi/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-hr/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-hu/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-in/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-it/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-iw/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-ja/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-ko/strings.xml55
-rw-r--r--platform/android/java/lib/res/values-lt/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-lv/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-nb/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-nl/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-pl/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-pt/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-ro/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-ru/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-sk/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-sl/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-sr/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-sv/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-th/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-tl/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-tr/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-uk/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-vi/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-zh-rCN/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-zh-rHK/strings.xml4
-rw-r--r--platform/android/java/lib/res/values-zh-rTW/strings.xml4
-rw-r--r--platform/android/java/lib/res/values/strings.xml55
-rw-r--r--platform/android/java/lib/res/values/styles.xml25
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java236
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java80
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java297
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java201
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java367
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java126
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java83
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java41
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java129
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java112
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java92
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java229
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java852
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java1346
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java510
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java200
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java110
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java414
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java47
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java389
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java67
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java231
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java32
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java48
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java65
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java80
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java81
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java300
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java100
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java33
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java578
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java32
-rw-r--r--platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java60
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Dictionary.java81
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java1111
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderAlarmReceiver.java59
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotDownloaderService.java84
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java631
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java50
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java214
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotPaymentV3.java230
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java61
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotView.java170
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java171
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java360
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java150
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java135
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java102
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/Joystick.java44
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/ConsumeTask.java116
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/HandlePurchaseTask.java93
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsCache.java72
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/PaymentsManager.java419
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/PurchaseTask.java118
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/ReleaseAllConsumablesTask.java141
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/payments/ValidateTask.java142
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java69
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/CustomSSLSocketFactory.java69
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java157
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/HttpRequester.java227
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/RequestParams.java85
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java51
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java112
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java58
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java60
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java151
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java81
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java61
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
new file mode 100644
index 0000000000..2c246b04a4
--- /dev/null
+++ b/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png
Binary files differ
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
new file mode 100644
index 0000000000..8bcd464bed
--- /dev/null
+++ b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png
Binary files differ
diff --git a/platform/android/java/lib/res/drawable-nodpi/icon.png b/platform/android/java/lib/res/drawable-nodpi/icon.png
new file mode 100644
index 0000000000..6ad9b43117
--- /dev/null
+++ b/platform/android/java/lib/res/drawable-nodpi/icon.png
Binary files differ
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
new file mode 100644
index 0000000000..372b763ec5
--- /dev/null
+++ b/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png
Binary files differ
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
new file mode 100644
index 0000000000..b458ff3057
--- /dev/null
+++ b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png
Binary files differ
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 &lt;application, user&gt; 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 &lt;application, user, device id&gt; 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;
+ }
+}