diff options
Diffstat (limited to 'platform/android/java/lib')
128 files changed, 15139 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..13a14422ed --- /dev/null +++ b/platform/android/java/lib/build.gradle @@ -0,0 +1,82 @@ +apply plugin: 'com.android.library' + +dependencies { +    implementation "com.android.support:support-core-utils:28.0.0" +} + +def pathToRootDir = "../../../../" + +android { +    compileSdkVersion versions.compileSdk +    buildToolsVersion versions.buildTools +    useLibrary 'org.apache.http.legacy' + +    defaultConfig { +        minSdkVersion versions.minSdk +        targetSdkVersion versions.targetSdk +    } + +    lintOptions { +        abortOnError false +        disable 'MissingTranslation', 'UnusedResources' +    } + +    packagingOptions { +        exclude 'META-INF/LICENSE' +        exclude 'META-INF/NOTICE' +    } + +    sourceSets { +        main { +            manifest.srcFile 'AndroidManifest.xml' +            java.srcDirs = ['src'] +            res.srcDirs = ['res'] +            aidl.srcDirs = ['aidl'] +            assets.srcDirs = ['assets'] +        } +        debug.jniLibs.srcDirs = ['libs/debug'] +        release.jniLibs.srcDirs = ['libs/release'] +    } + +    libraryVariants.all { variant -> +        variant.outputs.all { output -> +            output.outputFileName = "godot-lib.${variant.name}.aar" +        } + +        def buildType = variant.buildType.name.capitalize() + +        def taskPrefix = "" +        if (project.path != ":") { +            taskPrefix = project.path + ":" +        } + +        // Disable the externalNativeBuild* task as it would cause build failures since the cmake build +        // files is only setup for editing support. +        gradle.startParameter.excludedTaskNames += taskPrefix + "externalNativeBuild" + buildType + +        def releaseTarget = supportedTargets[buildType.toLowerCase()] +        if (releaseTarget == null || releaseTarget == "") { +            throw new GradleException("Invalid build type: " + buildType) +        } + +        if (!supportedAbis.contains(defaultAbi)) { +            throw new GradleException("Invalid default abi: " + defaultAbi) +        } + +        // Creating gradle task to generate the native libraries for the default abi. +        def taskName = getSconsTaskName(buildType) +        tasks.create(name: taskName, type: Exec) { +            executable "scons" + sconsExt +            args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() +        } + +        // Schedule the tasks so the generated libs are present before the aar file is packaged. +        tasks["merge${buildType}JniLibFolders"].dependsOn taskName +    } + +    externalNativeBuild { +        cmake { +            path "CMakeLists.txt" +        } +    } +} diff --git a/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch b/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch new file mode 100644 index 0000000000..49cc41e817 --- /dev/null +++ b/platform/android/java/lib/patches/com.google.android.vending.expansion.downloader.patch @@ -0,0 +1,300 @@ +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +index ad6ea0de6..452c7d148 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java +@@ -32,6 +32,9 @@ import android.os.Messenger; + import android.os.RemoteException; + import android.util.Log; +  ++// -- GODOT start -- ++import java.lang.ref.WeakReference; ++// -- GODOT end -- +  +  + /** +@@ -118,29 +121,46 @@ public class DownloaderClientMarshaller { +         /** +          * Target we publish for clients to send messages to IncomingHandler. +          */ +-        final Messenger mMessenger = new Messenger(new Handler() { ++        // -- GODOT start -- ++        private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); ++        final Messenger mMessenger = new Messenger(mMsgHandler); ++ ++        private static class MessengerHandlerClient extends Handler { ++            private final WeakReference<Stub> mDownloader; ++            public MessengerHandlerClient(Stub downloader) { ++                mDownloader = new WeakReference<>(downloader); ++            } ++ +             @Override +             public void handleMessage(Message msg) { +-                switch (msg.what) { +-                    case MSG_ONDOWNLOADPROGRESS: +-                        Bundle bun = msg.getData(); +-                        if ( null != mContext ) { +-                            bun.setClassLoader(mContext.getClassLoader()); +-                            DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData() +-                                    .getParcelable(PARAM_PROGRESS); +-                            mItf.onDownloadProgress(dpi); +-                        } +-                        break; +-                    case MSG_ONDOWNLOADSTATE_CHANGED: +-                        mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); +-                        break; +-                    case MSG_ONSERVICECONNECTED: +-                        mItf.onServiceConnected( +-                                (Messenger) msg.getData().getParcelable(PARAM_MESSENGER)); +-                        break; ++                Stub downloader = mDownloader.get(); ++                if (downloader != null) { ++                    downloader.handleMessage(msg); +                 } +             } +-        }); ++        } ++ ++        private void handleMessage(Message msg) { ++            switch (msg.what) { ++                case MSG_ONDOWNLOADPROGRESS: ++                    Bundle bun = msg.getData(); ++                    if (null != mContext) { ++                        bun.setClassLoader(mContext.getClassLoader()); ++                        DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData() ++                                                            .getParcelable(PARAM_PROGRESS); ++                        mItf.onDownloadProgress(dpi); ++                    } ++                    break; ++                case MSG_ONDOWNLOADSTATE_CHANGED: ++                    mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); ++                    break; ++                case MSG_ONSERVICECONNECTED: ++                    mItf.onServiceConnected( ++                            (Messenger)msg.getData().getParcelable(PARAM_MESSENGER)); ++                    break; ++            } ++        } ++        // -- GODOT end -- +  +         public Stub(IDownloaderClient itf, Class<?> downloaderService) { +             mItf = itf; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +index 979352299..3771d19c9 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java +@@ -25,6 +25,9 @@ import android.os.Message; + import android.os.Messenger; + import android.os.RemoteException; +  ++// -- GODOT start -- ++import java.lang.ref.WeakReference; ++// -- GODOT end -- +  +  + /** +@@ -108,32 +111,49 @@ public class DownloaderServiceMarshaller { +  +     private static class Stub implements IStub { +         private IDownloaderService mItf = null; +-        final Messenger mMessenger = new Messenger(new Handler() { ++        // -- GODOT start -- ++        private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); ++        final Messenger mMessenger = new Messenger(mMsgHandler); ++ ++        private static class MessengerHandlerServer extends Handler { ++            private final WeakReference<Stub> mDownloader; ++            public MessengerHandlerServer(Stub downloader) { ++                mDownloader = new WeakReference<>(downloader); ++            } ++ +             @Override +             public void handleMessage(Message msg) { +-                switch (msg.what) { +-                    case MSG_REQUEST_ABORT_DOWNLOAD: +-                        mItf.requestAbortDownload(); +-                        break; +-                    case MSG_REQUEST_CONTINUE_DOWNLOAD: +-                        mItf.requestContinueDownload(); +-                        break; +-                    case MSG_REQUEST_PAUSE_DOWNLOAD: +-                        mItf.requestPauseDownload(); +-                        break; +-                    case MSG_SET_DOWNLOAD_FLAGS: +-                        mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); +-                        break; +-                    case MSG_REQUEST_DOWNLOAD_STATE: +-                        mItf.requestDownloadStatus(); +-                        break; +-                    case MSG_REQUEST_CLIENT_UPDATE: +-                        mItf.onClientUpdated((Messenger) msg.getData().getParcelable( +-                                PARAM_MESSENGER)); +-                        break; ++                Stub downloader = mDownloader.get(); ++                if (downloader != null) { ++                    downloader.handleMessage(msg); +                 } +             } +-        }); ++        } ++ ++        private void handleMessage(Message msg) { ++            switch (msg.what) { ++                case MSG_REQUEST_ABORT_DOWNLOAD: ++                    mItf.requestAbortDownload(); ++                    break; ++                case MSG_REQUEST_CONTINUE_DOWNLOAD: ++                    mItf.requestContinueDownload(); ++                    break; ++                case MSG_REQUEST_PAUSE_DOWNLOAD: ++                    mItf.requestPauseDownload(); ++                    break; ++                case MSG_SET_DOWNLOAD_FLAGS: ++                    mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); ++                    break; ++                case MSG_REQUEST_DOWNLOAD_STATE: ++                    mItf.requestDownloadStatus(); ++                    break; ++                case MSG_REQUEST_CLIENT_UPDATE: ++                    mItf.onClientUpdated((Messenger)msg.getData().getParcelable( ++                            PARAM_MESSENGER)); ++                    break; ++            } ++        } ++        // -- GODOT end -- +  +         public Stub(IDownloaderService itf) { +             mItf = itf; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +index e4b1b0f1c..36cd6aacf 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/Helpers.java +@@ -24,7 +24,10 @@ import android.os.StatFs; + import android.os.SystemClock; + import android.util.Log; +  +-import com.android.vending.expansion.downloader.R; ++// -- GODOT start -- ++//import com.android.vending.expansion.downloader.R; ++import org.godotengine.godot.R; ++// -- GODOT end -- +  + import java.io.File; + import java.text.SimpleDateFormat; +@@ -146,12 +149,14 @@ public class Helpers { +             } +             return ""; +         } +-        return String.format("%.2f", ++        // -- GODOT start -- ++        return String.format(Locale.ENGLISH, "%.2f", +                 (float) overallProgress / (1024.0f * 1024.0f)) +                 + "MB /" + +-                String.format("%.2f", (float) overallTotal / ++                String.format(Locale.ENGLISH, "%.2f", (float) overallTotal / +                         (1024.0f * 1024.0f)) +                 + "MB"; ++        // -- GODOT end -- +     } +  +     /** +@@ -184,7 +189,9 @@ public class Helpers { +     } +  +     public static String getSpeedString(float bytesPerMillisecond) { +-        return String.format("%.2f", bytesPerMillisecond * 1000 / 1024); ++        // -- GODOT start -- ++        return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); ++        // -- GODOT end -- +     } +  +     public static String getTimeRemaining(long durationInMilliseconds) { +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +index 12edd97ab..a0e1165cc 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/SystemFacade.java +@@ -26,6 +26,10 @@ import android.net.NetworkInfo; + import android.telephony.TelephonyManager; + import android.util.Log; +  ++// -- GODOT start -- ++import android.annotation.SuppressLint; ++// -- GODOT end -- ++ + /** +  * Contains useful helper functions, typically tied to the application context. +  */ +@@ -51,6 +55,7 @@ class SystemFacade { +             return null; +         } +  ++        @SuppressLint("MissingPermission") +         NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); +         if (activeInfo == null) { +             if (Constants.LOGVV) { +@@ -69,6 +74,7 @@ class SystemFacade { +             return false; +         } +  ++        @SuppressLint("MissingPermission") +         NetworkInfo info = connectivity.getActiveNetworkInfo(); +         boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); +         TelephonyManager tm = (TelephonyManager) mContext +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +index f1536e80e..4b214b22d 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java +@@ -16,7 +16,11 @@ +  + package com.google.android.vending.expansion.downloader.impl; +  +-import com.android.vending.expansion.downloader.R; ++// -- GODOT start -- ++//import com.android.vending.expansion.downloader.R; ++import org.godotengine.godot.R; ++// -- GODOT end -- ++ + import com.google.android.vending.expansion.downloader.DownloadProgressInfo; + import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; + import com.google.android.vending.expansion.downloader.Helpers; +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +index b2e0e7af0..c114b8a64 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java +@@ -146,8 +146,12 @@ public class DownloadThread { +  +         try { +             PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); +-            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); +-            wakeLock.acquire(); ++            // -- GODOT start -- ++            //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); ++            //wakeLock.acquire(); ++            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); ++            wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); ++            // -- GODOT end -- +  +             if (Constants.LOGV) { +                 Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); +diff --git a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +index 4babe476f..8d41a7690 100644 +--- a/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java ++++ b/platform/android/java/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java +@@ -50,6 +50,10 @@ import android.provider.Settings.Secure; + import android.telephony.TelephonyManager; + import android.util.Log; +  ++// -- GODOT start -- ++import android.annotation.SuppressLint; ++// -- GODOT end -- ++ + import java.io.File; +  + /** +@@ -578,6 +582,7 @@ public abstract class DownloaderService extends CustomIntentService implements I +             Log.w(Constants.TAG, +                     "couldn't get connectivity manager to poll network state"); +         } else { ++            @SuppressLint("MissingPermission") +             NetworkInfo activeInfo = mConnectivityManager +                     .getActiveNetworkInfo(); +             updateNetworkState(activeInfo); diff --git a/platform/android/java/lib/patches/com.google.android.vending.licensing.patch b/platform/android/java/lib/patches/com.google.android.vending.licensing.patch new file mode 100644 index 0000000000..4adb81d951 --- /dev/null +++ b/platform/android/java/lib/patches/com.google.android.vending.licensing.patch @@ -0,0 +1,42 @@ +diff --git a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java +index 7c42bfc28..feb579af0 100644 +--- a/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java ++++ b/platform/android/java/src/com/google/android/vending/licensing/PreferenceObfuscator.java +@@ -45,6 +45,9 @@ public class PreferenceObfuscator { +     public void putString(String key, String value) { +         if (mEditor == null) { +             mEditor = mPreferences.edit(); ++            // -- GODOT start -- ++            mEditor.apply(); ++            // -- GODOT end -- +         } +         String obfuscatedValue = mObfuscator.obfuscate(value, key); +         mEditor.putString(key, obfuscatedValue); +diff --git a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java +index a0d2779af..a8bf65f9c 100644 +--- a/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java ++++ b/platform/android/java/src/com/google/android/vending/licensing/util/Base64.java +@@ -31,6 +31,10 @@ package com.google.android.vending.licensing.util; +  * @version 1.3 +  */ +  ++// -- GODOT start -- +import org.godotengine.godot.BuildConfig; ++// -- GODOT end -- ++ + /** +  * Base64 converter class. This code is not a full-blown MIME encoder; +  * it simply converts binary data to base64 data and back. +@@ -341,7 +345,11 @@ public class Base64 { +       e += 4; +     } +  +-    assert (e == outBuff.length); ++    // -- GODOT start -- ++    //assert (e == outBuff.length); ++    if (BuildConfig.DEBUG && e != outBuff.length) ++      throw new RuntimeException(); ++    // -- GODOT end -- +     return outBuff; +   } +  diff --git a/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..f849d8e90d --- /dev/null +++ b/platform/android/java/lib/res/drawable-hdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..1dfb28b33a --- /dev/null +++ b/platform/android/java/lib/res/drawable-mdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-nodpi/icon.png b/platform/android/java/lib/res/drawable-nodpi/icon.png Binary files differnew file mode 100644 index 0000000000..6ad9b43117 --- /dev/null +++ b/platform/android/java/lib/res/drawable-nodpi/icon.png diff --git a/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..372b763ec5 --- /dev/null +++ b/platform/android/java/lib/res/drawable-xhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png Binary files differnew file mode 100644 index 0000000000..302a972049 --- /dev/null +++ b/platform/android/java/lib/res/drawable-xxhdpi/notify_panel_notification_icon_bg.png diff --git a/platform/android/java/lib/res/layout/downloading_expansion.xml b/platform/android/java/lib/res/layout/downloading_expansion.xml new file mode 100644 index 0000000000..4a9700965f --- /dev/null +++ b/platform/android/java/lib/res/layout/downloading_expansion.xml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:orientation="vertical" > + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_weight="0" +        android:orientation="vertical" > + +        <TextView +            android:id="@+id/statusText" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_marginBottom="10dp" +            android:layout_marginStart="5dp" +            android:layout_marginTop="10dp" +            android:textStyle="bold" /> + +        <LinearLayout +            android:id="@+id/downloaderDashboard" +            android:layout_width="fill_parent" +            android:layout_height="wrap_content" +            android:orientation="vertical" > + +            <RelativeLayout +                android:layout_width="match_parent" +                android:layout_height="0dp" +                android:layout_weight="1" > + +                <TextView +                    android:id="@+id/progressAsFraction" +                    style="@android:style/TextAppearance.Small" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_alignParentStart="true" +                    android:layout_marginStart="5dp" /> + +                <TextView +                    android:id="@+id/progressAsPercentage" +                    style="@android:style/TextAppearance.Small" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_alignEnd="@+id/progressBar" /> + +                <ProgressBar +                    android:id="@+id/progressBar" +                    style="?android:attr/progressBarStyleHorizontal" +                    android:layout_width="match_parent" +                    android:layout_height="wrap_content" +                    android:layout_below="@+id/progressAsFraction" +                    android:layout_marginBottom="10dp" +                    android:layout_marginLeft="10dp" +                    android:layout_marginRight="10dp" +                    android:layout_marginTop="10dp" /> + +                <TextView +                    android:id="@+id/progressAverageSpeed" +                    style="@android:style/TextAppearance.Small" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_alignParentStart="true" +                    android:layout_below="@+id/progressBar" +                    android:layout_marginStart="5dp" /> + +                <TextView +                    android:id="@+id/progressTimeRemaining" +                    style="@android:style/TextAppearance.Small" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_alignEnd="@+id/progressBar" +                    android:layout_below="@+id/progressBar" /> +            </RelativeLayout> + +            <LinearLayout +                android:id="@+id/downloadButton" +                android:layout_width="fill_parent" +                android:layout_height="wrap_content" +                android:orientation="horizontal" > + +                <Button +                    android:id="@+id/cancelButton" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_gravity="center_vertical" +                    android:layout_marginBottom="10dp" +                    android:layout_marginLeft="5dp" +                    android:layout_marginRight="5dp" +                    android:layout_marginTop="10dp" +                    android:layout_weight="0" +                    android:minHeight="40dp" +                    android:minWidth="94dp" +                    android:text="@string/text_button_cancel" +                    android:visibility="gone" +                    style="?android:attr/buttonBarButtonStyle" /> + +                <Button +                    android:id="@+id/pauseButton" +                    android:layout_width="wrap_content" +                    android:layout_height="wrap_content" +                    android:layout_gravity="center_vertical" +                    android:layout_marginBottom="10dp" +                    android:layout_marginStart="10dp" +                    android:layout_marginEnd="5dp" +                    android:layout_marginTop="10dp" +                    android:layout_weight="0" +                    android:minHeight="40dp" +                    android:minWidth="94dp" +                    android:text="@string/text_button_pause" +                    style="?android:attr/buttonBarButtonStyle" /> +            </LinearLayout> +        </LinearLayout> +    </LinearLayout> + +    <LinearLayout +        android:id="@+id/approveCellular" +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:layout_weight="1" +        android:orientation="vertical" +        android:visibility="gone" > + +        <TextView +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_margin="10dp" +            android:id="@+id/textPausedParagraph1" +            android:text="@string/text_paused_cellular" /> + +        <TextView +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_margin="10dp" +            android:id="@+id/textPausedParagraph2" +            android:text="@string/text_paused_cellular_2" /> + +        <LinearLayout +            android:id="@+id/buttonRow" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:orientation="horizontal" > + +            <Button +                android:id="@+id/resumeOverCellular" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:layout_gravity="center" +                android:layout_margin="10dp" +                android:text="@string/text_button_resume_cellular" +                style="?android:attr/buttonBarButtonStyle" /> + +            <Button +                android:id="@+id/wifiSettingsButton" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:layout_gravity="center" +                android:layout_margin="10dp" +                android:text="@string/text_button_wifi_settings" +                style="?android:attr/buttonBarButtonStyle" /> +            </LinearLayout> +    </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml new file mode 100644 index 0000000000..fae1faeb60 --- /dev/null +++ b/platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2008, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +**     http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<LinearLayout xmlns:tools="http://schemas.android.com/tools" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:baselineAligned="false" +    android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android"> + +    <RelativeLayout +        android:layout_width="35dp" +        android:layout_height="fill_parent" +        android:paddingTop="10dp" +        android:paddingBottom="8dp" > + +        <ImageView +            android:id="@+id/appIcon" +            android:layout_width="fill_parent" +            android:layout_height="25dp" +            android:scaleType="centerInside" +            android:layout_alignParentStart="true" +            android:layout_alignParentTop="true" +            android:src="@android:drawable/stat_sys_download" +            android:contentDescription="@string/godot_project_name_string" /> + +        <TextView +            android:id="@+id/progress_text" +            style="@style/NotificationText" +            android:layout_width="fill_parent" +            android:layout_height="wrap_content" +            android:layout_alignParentStart="true" +            android:layout_alignParentBottom="true" +            android:layout_gravity="center_horizontal" +            android:singleLine="true" +            android:gravity="center" /> +    </RelativeLayout> + +    <RelativeLayout +        android:layout_width="0dip" +        android:layout_height="match_parent" +        android:layout_weight="1.0" +        android:clickable="true" +        android:focusable="true" +        android:paddingTop="10dp" +        android:paddingEnd="8dp" +        android:paddingBottom="8dp" +        tools:ignore="RtlSymmetry"> + +        <TextView +            android:id="@+id/title" +            style="@style/NotificationTitle" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_alignParentStart="true" +            android:singleLine="true"/> + +        <TextView +            android:id="@+id/time_remaining" +            style="@style/NotificationText" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_alignParentEnd="true" +            android:singleLine="true" +            tools:ignore="RelativeOverlap" /> +        <!-- Only one of progress_bar and paused_text will be visible. --> + +        <FrameLayout +            android:id="@+id/progress_bar_frame" +            android:layout_width="fill_parent" +            android:layout_height="wrap_content" +            android:layout_alignParentBottom="true" > + +            <ProgressBar +                android:id="@+id/progress_bar" +                style="?android:attr/progressBarStyleHorizontal" +                android:layout_width="fill_parent" +                android:layout_height="wrap_content" +                android:paddingEnd="25dp" /> + +            <TextView +                android:id="@+id/description" +                style="@style/NotificationTextShadow" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:layout_gravity="center" +                android:paddingEnd="25dp" +                android:singleLine="true" /> +        </FrameLayout> + +    </RelativeLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ar/strings.xml b/platform/android/java/lib/res/values-ar/strings.xml new file mode 100644 index 0000000000..9f3dc6d6ac --- /dev/null +++ b/platform/android/java/lib/res/values-ar/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ar</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-bg/strings.xml b/platform/android/java/lib/res/values-bg/strings.xml new file mode 100644 index 0000000000..bd8109277e --- /dev/null +++ b/platform/android/java/lib/res/values-bg/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-bg</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ca/strings.xml b/platform/android/java/lib/res/values-ca/strings.xml new file mode 100644 index 0000000000..494cb88468 --- /dev/null +++ b/platform/android/java/lib/res/values-ca/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ca</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-cs/strings.xml b/platform/android/java/lib/res/values-cs/strings.xml new file mode 100644 index 0000000000..30ce00f895 --- /dev/null +++ b/platform/android/java/lib/res/values-cs/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-cs</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-da/strings.xml b/platform/android/java/lib/res/values-da/strings.xml new file mode 100644 index 0000000000..4c2a1cf0f4 --- /dev/null +++ b/platform/android/java/lib/res/values-da/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-da</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-de/strings.xml b/platform/android/java/lib/res/values-de/strings.xml new file mode 100644 index 0000000000..52946d4cce --- /dev/null +++ b/platform/android/java/lib/res/values-de/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-de</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-el/strings.xml b/platform/android/java/lib/res/values-el/strings.xml new file mode 100644 index 0000000000..181dc51762 --- /dev/null +++ b/platform/android/java/lib/res/values-el/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-el</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-en/strings.xml b/platform/android/java/lib/res/values-en/strings.xml new file mode 100644 index 0000000000..976a565013 --- /dev/null +++ b/platform/android/java/lib/res/values-en/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-en</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-es-rES/strings.xml b/platform/android/java/lib/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..73f63a08f8 --- /dev/null +++ b/platform/android/java/lib/res/values-es-rES/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-es_ES</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-es/strings.xml b/platform/android/java/lib/res/values-es/strings.xml new file mode 100644 index 0000000000..07b718a641 --- /dev/null +++ b/platform/android/java/lib/res/values-es/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-es</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-fa/strings.xml b/platform/android/java/lib/res/values-fa/strings.xml new file mode 100644 index 0000000000..f1e29013c4 --- /dev/null +++ b/platform/android/java/lib/res/values-fa/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-fa</string> +    <string name="text_paused_cellular">آیا می خواهید بر روی اتصال داده همراه دانلود را شروع کنید؟ بر اساس نوع سطح داده شما این ممکن است برای شما هزینه مالی داشته باشد.</string> +    <string name="text_paused_cellular_2">اگر نمی خواهید بر روی اتصال داده همراه دانلود را شروع کنید ، دانلود به صورت خودکار در زمان دسترسی به وای-فای شروع می شود.</string> +    <string name="text_button_resume_cellular">ادامه دانلود</string> +    <string name="text_button_wifi_settings">تنظیمات وای-فای</string> +    <string name="text_verifying_download">درحال تایید دانلود</string> +    <string name="text_validation_complete">تایید فایل XAPK تکمیل شد.  برای خروج تایید کنید.</string> +    <string name="text_validation_failed">اعتبارسنجی فایل XAPK ناموق.</string> +    <string name="text_button_pause">توقف دانلود</string> +    <string name="text_button_resume">ادامه دانلود</string> +    <string name="text_button_cancel">انصراف</string> +    <string name="text_button_cancel_verify">انصراف از تایید شدن</string> +</resources> diff --git a/platform/android/java/lib/res/values-fi/strings.xml b/platform/android/java/lib/res/values-fi/strings.xml new file mode 100644 index 0000000000..323d82aff1 --- /dev/null +++ b/platform/android/java/lib/res/values-fi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-fi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-fr/strings.xml b/platform/android/java/lib/res/values-fr/strings.xml new file mode 100644 index 0000000000..32bead2661 --- /dev/null +++ b/platform/android/java/lib/res/values-fr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-fr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hi/strings.xml b/platform/android/java/lib/res/values-hi/strings.xml new file mode 100644 index 0000000000..8aab2a8c63 --- /dev/null +++ b/platform/android/java/lib/res/values-hi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-hi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hr/strings.xml b/platform/android/java/lib/res/values-hr/strings.xml new file mode 100644 index 0000000000..caf55e2241 --- /dev/null +++ b/platform/android/java/lib/res/values-hr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-hr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-hu/strings.xml b/platform/android/java/lib/res/values-hu/strings.xml new file mode 100644 index 0000000000..e7f9e51226 --- /dev/null +++ b/platform/android/java/lib/res/values-hu/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-hu</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-in/strings.xml b/platform/android/java/lib/res/values-in/strings.xml new file mode 100644 index 0000000000..9e9a8b0c03 --- /dev/null +++ b/platform/android/java/lib/res/values-in/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-id</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-it/strings.xml b/platform/android/java/lib/res/values-it/strings.xml new file mode 100644 index 0000000000..1f5e5a049e --- /dev/null +++ b/platform/android/java/lib/res/values-it/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-it</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-iw/strings.xml b/platform/android/java/lib/res/values-iw/strings.xml new file mode 100644 index 0000000000..f52ede2085 --- /dev/null +++ b/platform/android/java/lib/res/values-iw/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-he</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ja/strings.xml b/platform/android/java/lib/res/values-ja/strings.xml new file mode 100644 index 0000000000..7f85f57df7 --- /dev/null +++ b/platform/android/java/lib/res/values-ja/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ja</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ko/strings.xml b/platform/android/java/lib/res/values-ko/strings.xml new file mode 100644 index 0000000000..fab0bdd753 --- /dev/null +++ b/platform/android/java/lib/res/values-ko/strings.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ko</string> +    <string name="text_paused_cellular">모바일 네트워크를 사용하여 다운로드 하시겠습니까? 남은 데이터 사용량에 따라, 요금이 부과될 수 있습니다.</string> +    <string name="text_paused_cellular_2">모바일 네트워크를 사용하여 다운로드 하지 않을 경우, 와이파이 연결이 가능할 때 자동적으로 다운로드가 이루어집니다.</string> +    <string name="text_button_resume_cellular">다운로드 계속하기</string> +    <string name="text_button_wifi_settings">와이파이 설정</string> +    <string name="text_verifying_download">다운로드 확인중</string> +    <string name="text_validation_complete">추가 파일 확인이 완료되었습니다. 확인을 눌러 진행하세요.</string> +    <string name="text_validation_failed">추가 파일 확인에 실패하였습니다.</string> +    <string name="text_button_pause">다운로드 일시정지</string> +    <string name="text_button_resume">다운로드 계속하기</string> +    <string name="text_button_cancel">취소</string> +    <string name="text_button_cancel_verify">파일 확인 취소</string> + +    <!-- APK Expansion Strings --> + +    <!-- When a download completes, a notification is displayed, and this +        string is used to indicate that the download successfully completed. +        Note that such a download could have been initiated by a variety of +        applications, including (but not limited to) the browser, an email +        application, a content marketplace. --> +    <string name="notification_download_complete">다운로드 완료</string> + +    <!-- When a download completes, a notification is displayed, and this +        string is used to indicate that the download failed. +        Note that such a download could have been initiated by a variety of +        applications, including (but not limited to) the browser, an email +        application, a content marketplace. --> +    <string name="notification_download_failed">다운로드 실패</string> + + +    <string name="state_unknown">시작중…</string> +    <string name="state_idle">다운로드 시작을 기다리는 중</string> +    <string name="state_fetching_url">다운로드할 항목을 찾는 중</string> +    <string name="state_connecting">다운로드 서버에 연결 중</string> +    <string name="state_downloading">다운로드 중</string> +    <string name="state_completed">다운로드 종료</string> +    <string name="state_paused_network_unavailable">와이파이를 찾을 수 없어 다운로드가 일시정지 되었습니다.</string> +    <string name="state_paused_network_setup_failure">다운로드가 일시정지 되었습니다. 네트워크 연결 상태를 확인하세요.</string> +    <string name="state_paused_by_request">다운로드 일시정지</string> +    <string name="state_paused_wifi_unavailable">와이파이가 사용하능하지 않아 다운로드가 일시정지 되었습니다.</string> +    <string name="state_paused_wifi_disabled">와이파이가 비활성화 되어 다운로드가 일시정지 되었습니다.</string> +    <string name="state_paused_roaming">로밍 상태이어서 다운로드가 일시정지 되었습니다.</string> +    <string name="state_paused_sdcard_unavailable">외부 저장소를 사용할 수 없어 다운로드가 일시정지 되었습니다.</string> +    <string name="state_failed_unlicensed">이 앱을 구매하지 않아 다운로드가 정지 되었습니다.</string> +    <string name="state_failed_fetching_url">다운로드 항목을 찾을 수 없어 다운로드가 정지 되었습니다.</string> +    <string name="state_failed_sdcard_full">외부 저장소가 가득차서 다운로드가 실패하였습니다.</string> +    <string name="state_failed_cancelled">다운로드 취소</string> +    <string name="state_failed">다운로드 실패</string> + +    <string name="kilobytes_per_second">%1$s KB/s</string> +    <string name="time_remaining">남은 시간: %1$s</string> +    <string name="time_remaining_notification">%1$s 남음</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-lt/strings.xml b/platform/android/java/lib/res/values-lt/strings.xml new file mode 100644 index 0000000000..6e3677fde7 --- /dev/null +++ b/platform/android/java/lib/res/values-lt/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-lt</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-lv/strings.xml b/platform/android/java/lib/res/values-lv/strings.xml new file mode 100644 index 0000000000..701fc271ac --- /dev/null +++ b/platform/android/java/lib/res/values-lv/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-lv</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-nb/strings.xml b/platform/android/java/lib/res/values-nb/strings.xml new file mode 100644 index 0000000000..73147ca1af --- /dev/null +++ b/platform/android/java/lib/res/values-nb/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-nb</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-nl/strings.xml b/platform/android/java/lib/res/values-nl/strings.xml new file mode 100644 index 0000000000..e501928a35 --- /dev/null +++ b/platform/android/java/lib/res/values-nl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-nl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-pl/strings.xml b/platform/android/java/lib/res/values-pl/strings.xml new file mode 100644 index 0000000000..ea5da73b6f --- /dev/null +++ b/platform/android/java/lib/res/values-pl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-pl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-pt/strings.xml b/platform/android/java/lib/res/values-pt/strings.xml new file mode 100644 index 0000000000..bdda7cd2c7 --- /dev/null +++ b/platform/android/java/lib/res/values-pt/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-pt</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ro/strings.xml b/platform/android/java/lib/res/values-ro/strings.xml new file mode 100644 index 0000000000..3686da4c19 --- /dev/null +++ b/platform/android/java/lib/res/values-ro/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ro</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-ru/strings.xml b/platform/android/java/lib/res/values-ru/strings.xml new file mode 100644 index 0000000000..954067658b --- /dev/null +++ b/platform/android/java/lib/res/values-ru/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-ru</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sk/strings.xml b/platform/android/java/lib/res/values-sk/strings.xml new file mode 100644 index 0000000000..37d1283124 --- /dev/null +++ b/platform/android/java/lib/res/values-sk/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-sk</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sl/strings.xml b/platform/android/java/lib/res/values-sl/strings.xml new file mode 100644 index 0000000000..0bb249c375 --- /dev/null +++ b/platform/android/java/lib/res/values-sl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-sl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sr/strings.xml b/platform/android/java/lib/res/values-sr/strings.xml new file mode 100644 index 0000000000..0e83cab1a1 --- /dev/null +++ b/platform/android/java/lib/res/values-sr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-sr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-sv/strings.xml b/platform/android/java/lib/res/values-sv/strings.xml new file mode 100644 index 0000000000..e3a04ac2ec --- /dev/null +++ b/platform/android/java/lib/res/values-sv/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-sv</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-th/strings.xml b/platform/android/java/lib/res/values-th/strings.xml new file mode 100644 index 0000000000..0aa893b8bf --- /dev/null +++ b/platform/android/java/lib/res/values-th/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-th</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-tl/strings.xml b/platform/android/java/lib/res/values-tl/strings.xml new file mode 100644 index 0000000000..e7e2af4909 --- /dev/null +++ b/platform/android/java/lib/res/values-tl/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-tl</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-tr/strings.xml b/platform/android/java/lib/res/values-tr/strings.xml new file mode 100644 index 0000000000..97af1243a6 --- /dev/null +++ b/platform/android/java/lib/res/values-tr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-tr</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-uk/strings.xml b/platform/android/java/lib/res/values-uk/strings.xml new file mode 100644 index 0000000000..3dea6908a9 --- /dev/null +++ b/platform/android/java/lib/res/values-uk/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-uk</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-vi/strings.xml b/platform/android/java/lib/res/values-vi/strings.xml new file mode 100644 index 0000000000..a6552130b0 --- /dev/null +++ b/platform/android/java/lib/res/values-vi/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-vi</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values-zh-rCN/strings.xml b/platform/android/java/lib/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..6668c56bd9 --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rCN/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-zh</string> +</resources> diff --git a/platform/android/java/lib/res/values-zh-rHK/strings.xml b/platform/android/java/lib/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..8a6269da0f --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rHK/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-zh_HK</string> +</resources> diff --git a/platform/android/java/lib/res/values-zh-rTW/strings.xml b/platform/android/java/lib/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..b1bb39d5d6 --- /dev/null +++ b/platform/android/java/lib/res/values-zh-rTW/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name-zh_TW</string> +</resources> diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml new file mode 100644 index 0000000000..a1b81a6186 --- /dev/null +++ b/platform/android/java/lib/res/values/strings.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +    <string name="godot_project_name_string">godot-project-name</string> +    <string name="text_paused_cellular">Would you like to enable downloading over cellular connections? Depending on your data plan, this may cost you money.</string> +    <string name="text_paused_cellular_2">If you choose not to enable downloading over cellular connections, the download will automatically resume when wi-fi is available.</string> +    <string name="text_button_resume_cellular">Resume download</string> +    <string name="text_button_wifi_settings">Wi-Fi settings</string> +    <string name="text_verifying_download">Verifying Download</string> +    <string name="text_validation_complete">XAPK File Validation Complete.  Select OK to exit.</string> +    <string name="text_validation_failed">XAPK File Validation Failed.</string> +    <string name="text_button_pause">Pause Download</string> +    <string name="text_button_resume">Resume Download</string> +    <string name="text_button_cancel">Cancel</string> +    <string name="text_button_cancel_verify">Cancel Verification</string> + +    <!-- APK Expansion Strings --> + +    <!-- When a download completes, a notification is displayed, and this +        string is used to indicate that the download successfully completed. +        Note that such a download could have been initiated by a variety of +        applications, including (but not limited to) the browser, an email +        application, a content marketplace. --> +    <string name="notification_download_complete">Download complete</string> + +    <!-- When a download completes, a notification is displayed, and this +        string is used to indicate that the download failed. +        Note that such a download could have been initiated by a variety of +        applications, including (but not limited to) the browser, an email +        application, a content marketplace. --> +    <string name="notification_download_failed">Download unsuccessful</string> + + +    <string name="state_unknown">Starting…</string> +    <string name="state_idle">Waiting for download to start</string> +    <string name="state_fetching_url">Looking for resources to download</string> +    <string name="state_connecting">Connecting to the download server</string> +    <string name="state_downloading">Downloading resources</string> +    <string name="state_completed">Download finished</string> +    <string name="state_paused_network_unavailable">Download paused because no network is available</string> +    <string name="state_paused_network_setup_failure">Download paused. Test a website in browser</string> +    <string name="state_paused_by_request">Download paused</string> +    <string name="state_paused_wifi_unavailable">Download paused because wifi is unavailable</string> +    <string name="state_paused_wifi_disabled">Download paused because wifi is disabled</string> +    <string name="state_paused_roaming">Download paused because you are roaming</string> +    <string name="state_paused_sdcard_unavailable">Download paused because the external storage is unavailable</string> +    <string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string> +    <string name="state_failed_fetching_url">Download failed because the resources could not be found</string> +    <string name="state_failed_sdcard_full">Download failed because the external storage is full</string> +    <string name="state_failed_cancelled">Download cancelled</string> +    <string name="state_failed">Download failed</string> + +    <string name="kilobytes_per_second">%1$s KB/s</string> +    <string name="time_remaining">Time remaining: %1$s</string> +    <string name="time_remaining_notification">%1$s left</string> +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/res/values/styles.xml b/platform/android/java/lib/res/values/styles.xml new file mode 100644 index 0000000000..a442f61e7e --- /dev/null +++ b/platform/android/java/lib/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + +    <style name="NotificationText"> +        <item name="android:textColor">?android:attr/textColorPrimary</item> +    </style> + +    <style name="NotificationTextShadow" parent="NotificationText"> +        <item name="android:textColor">?android:attr/textColorPrimary</item> +        <item name="android:shadowColor">@android:color/background_dark</item> +        <item name="android:shadowDx">1.0</item> +        <item name="android:shadowDy">1.0</item> +        <item name="android:shadowRadius">1</item> +    </style> + +    <style name="NotificationTitle"> +        <item name="android:textColor">?android:attr/textColorPrimary</item> +        <item name="android:textStyle">bold</item> +    </style> + +    <style name="ButtonBackground"> +        <item name="android:background">@android:color/background_dark</item> +    </style> + +</resources>
\ No newline at end of file diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java new file mode 100644 index 0000000000..1dcc370d83 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Constants.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import java.io.File; + + +/** + * Contains the internal constants that are used in the download manager. + * As a general rule, modifying these constants should be done with care. + */ +public class Constants { +    /** Tag used for debugging/logging */ +    public static final String TAG = "LVLDL"; + +    /** +     * Expansion path where we store obb files +     */ +    public static final String EXP_PATH = File.separator + "Android" +            + File.separator + "obb" + File.separator; + +    /** The intent that gets sent when the service must wake up for a retry */ +    public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; + +    /** the intent that gets sent when clicking a successful download */ +    public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; + +    /** the intent that gets sent when clicking an incomplete/failed download  */ +    public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; + +    /** the intent that gets sent when deleting the notification of a completed download */ +    public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; + +    /** +     * When a number has to be appended to the filename, this string is used to separate the +     * base filename from the sequence number +     */ +    public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; + +    /** The default user agent used for downloads */ +    public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; + +    /** The buffer size used to stream the data */ +    public static final int BUFFER_SIZE = 4096; + +    /** The minimum amount of progress that has to be done before the progress bar gets updated */ +    public static final int MIN_PROGRESS_STEP = 4096; + +    /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ +    public static final long MIN_PROGRESS_TIME = 1000; + +    /** The maximum number of rows in the database (FIFO) */ +    public static final int MAX_DOWNLOADS = 1000; + +    /** +     * The number of times that the download manager will retry its network +     * operations when no progress is happening before it gives up. +     */ +    public static final int MAX_RETRIES = 5; + +    /** +     * The minimum amount of time that the download manager accepts for +     * a Retry-After response header with a parameter in delta-seconds. +     */ +    public static final int MIN_RETRY_AFTER = 30; // 30s + +    /** +     * The maximum amount of time that the download manager accepts for +     * a Retry-After response header with a parameter in delta-seconds. +     */ +    public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h + +    /** +     * The maximum number of redirects. +     */ +    public static final int MAX_REDIRECTS = 5; // can't be more than 7. + +    /** +     * The time between a failure and the first retry after an IOException. +     * Each subsequent retry grows exponentially, doubling each time. +     * The time is in seconds. +     */ +    public static final int RETRY_FIRST_DELAY = 30; + +    /** Enable separate connectivity logging */ +    public static final boolean LOGX = true; + +    /** Enable verbose logging */ +    public static final boolean LOGV = false; + +    /** Enable super-verbose logging */ +    private static final boolean LOCAL_LOGVV = false; +    public static final boolean LOGVV = LOCAL_LOGVV && LOGV; + +    /** +     * This download has successfully completed. +     * Warning: there might be other status values that indicate success +     * in the future. +     * Use isSucccess() to capture the entire category. +     */ +    public static final int STATUS_SUCCESS = 200; + +    /** +     * This request couldn't be parsed. This is also used when processing +     * requests with unknown/unsupported URI schemes. +     */ +    public static final int STATUS_BAD_REQUEST = 400; + +    /** +     * This download can't be performed because the content type cannot be +     * handled. +     */ +    public static final int STATUS_NOT_ACCEPTABLE = 406; + +    /** +     * This download cannot be performed because the length cannot be +     * determined accurately. This is the code for the HTTP error "Length +     * Required", which is typically used when making requests that require +     * a content length but don't have one, and it is also used in the +     * client when a response is received whose length cannot be determined +     * accurately (therefore making it impossible to know when a download +     * completes). +     */ +    public static final int STATUS_LENGTH_REQUIRED = 411; + +    /** +     * This download was interrupted and cannot be resumed. +     * This is the code for the HTTP error "Precondition Failed", and it is +     * also used in situations where the client doesn't have an ETag at all. +     */ +    public static final int STATUS_PRECONDITION_FAILED = 412; + +    /** +     * The lowest-valued error status that is not an actual HTTP status code. +     */ +    public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; + +    /** +     * The requested destination file already exists. +     */ +    public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + +    /** +     * Some possibly transient error occurred, but we can't resume the download. +     */ +    public static final int STATUS_CANNOT_RESUME = 489; + +    /** +     * This download was canceled +     */ +    public static final int STATUS_CANCELED = 490; + +    /** +     * This download has completed with an error. +     * Warning: there will be other status values that indicate errors in +     * the future. Use isStatusError() to capture the entire category. +     */ +    public static final int STATUS_UNKNOWN_ERROR = 491; + +    /** +     * This download couldn't be completed because of a storage issue. +     * Typically, that's because the filesystem is missing or full. +     * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} +     * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. +     */ +    public static final int STATUS_FILE_ERROR = 492; + +    /** +     * This download couldn't be completed because of an HTTP +     * redirect response that the download manager couldn't +     * handle. +     */ +    public static final int STATUS_UNHANDLED_REDIRECT = 493; + +    /** +     * This download couldn't be completed because of an +     * unspecified unhandled HTTP code. +     */ +    public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + +    /** +     * This download couldn't be completed because of an +     * error receiving or processing data at the HTTP level. +     */ +    public static final int STATUS_HTTP_DATA_ERROR = 495; + +    /** +     * This download couldn't be completed because of an +     * HttpException while setting up the request. +     */ +    public static final int STATUS_HTTP_EXCEPTION = 496; + +    /** +     * This download couldn't be completed because there were +     * too many redirects. +     */ +    public static final int STATUS_TOO_MANY_REDIRECTS = 497; + +    /** +     * This download couldn't be completed due to insufficient storage +     * space.  Typically, this is because the SD card is full. +     */ +    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + +    /** +     * This download couldn't be completed because no external storage +     * device was found.  Typically, this is because the SD card is not +     * mounted. +     */ +    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + +    /** +     * The wake duration to check to see if a download is possible. +     */ +    public static final long WATCHDOG_WAKE_TIMER = 60*1000; + +    /** +     * The wake duration to check to see if the process was killed. +     */ +    public static final long ACTIVE_THREAD_WATCHDOG = 5*1000; + +}
\ No newline at end of file diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java new file mode 100644 index 0000000000..9cb294d721 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloadProgressInfo.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * This class contains progress information about the active download(s). + * + * When you build the Activity that initiates a download and tracks the + * progress by implementing the {@link IDownloaderClient} interface, you'll + * receive a DownloadProgressInfo object in each call to the {@link + * IDownloaderClient#onDownloadProgress} method. This allows you to update + * your activity's UI with information about the download progress, such + * as the progress so far, time remaining and current speed. + */ +public class DownloadProgressInfo implements Parcelable { +    public long mOverallTotal; +    public long mOverallProgress; +    public long mTimeRemaining; // time remaining +    public float mCurrentSpeed; // speed in KB/S + +    @Override +    public int describeContents() { +        return 0; +    } + +    @Override +    public void writeToParcel(Parcel p, int i) { +        p.writeLong(mOverallTotal); +        p.writeLong(mOverallProgress); +        p.writeLong(mTimeRemaining); +        p.writeFloat(mCurrentSpeed); +    } + +    public DownloadProgressInfo(Parcel p) { +        mOverallTotal = p.readLong(); +        mOverallProgress = p.readLong(); +        mTimeRemaining = p.readLong(); +        mCurrentSpeed = p.readFloat(); +    } + +    public DownloadProgressInfo(long overallTotal, long overallProgress, +            long timeRemaining, +            float currentSpeed) { +        this.mOverallTotal = overallTotal; +        this.mOverallProgress = overallProgress; +        this.mTimeRemaining = timeRemaining; +        this.mCurrentSpeed = currentSpeed; +    } + +    public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { +        @Override +        public DownloadProgressInfo createFromParcel(Parcel parcel) { +            return new DownloadProgressInfo(parcel); +        } + +        @Override +        public DownloadProgressInfo[] newArray(int i) { +            return new DownloadProgressInfo[i]; +        } +    }; + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java new file mode 100644 index 0000000000..452c7d1483 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderClientMarshaller.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +// -- GODOT start -- +import java.lang.ref.WeakReference; +// -- GODOT end -- + + +/** + * This class binds the service API to your application client.  It contains the IDownloaderClient proxy, + * which is used to call functions in your client as well as the Stub, which is used to call functions + * in the client implementation of IDownloaderClient. + * + * <p>The IPC is implemented using an Android Messenger and a service Binder.  The connect method + * should be called whenever the client wants to bind to the service.  It opens up a service connection + * that ends up calling the onServiceConnected client API that passes the service messenger + * in.  If the client wants to be notified by the service, it is responsible for then passing its + * messenger to the service in a separate call. + * + * <p>Critical methods are {@link #startDownloadServiceIfRequired} and {@link #CreateStub}. + * + * <p>When your application first starts, you should first check whether your app's expansion files are + * already on the device. If not, you should then call {@link #startDownloadServiceIfRequired}, which + * starts your {@link impl.DownloaderService} to download the expansion files if necessary. The method + * returns a value indicating whether download is required or not. + * + * <p>If a download is required, {@link #startDownloadServiceIfRequired} begins the download through + * the specified service and you should then call {@link #CreateStub} to instantiate a member {@link + * IStub} object that you need in order to receive calls through your {@link IDownloaderClient} + * interface. + */ +public class DownloaderClientMarshaller { +    public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; +    public static final int MSG_ONDOWNLOADPROGRESS = 11; +    public static final int MSG_ONSERVICECONNECTED = 12; + +    public static final String PARAM_NEW_STATE = "newState"; +    public static final String PARAM_PROGRESS = "progress"; +    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + +    public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; +    public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; +    public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; + +    private static class Proxy implements IDownloaderClient { +        private Messenger mServiceMessenger; + +        @Override +        public void onDownloadStateChanged(int newState) { +            Bundle params = new Bundle(1); +            params.putInt(PARAM_NEW_STATE, newState); +            send(MSG_ONDOWNLOADSTATE_CHANGED, params); +        } + +        @Override +        public void onDownloadProgress(DownloadProgressInfo progress) { +            Bundle params = new Bundle(1); +            params.putParcelable(PARAM_PROGRESS, progress); +            send(MSG_ONDOWNLOADPROGRESS, params); +        } + +        private void send(int method, Bundle params) { +            Message m = Message.obtain(null, method); +            m.setData(params); +            try { +                mServiceMessenger.send(m); +            } catch (RemoteException e) { +                e.printStackTrace(); +            } +        } + +        public Proxy(Messenger msg) { +            mServiceMessenger = msg; +        } + +        @Override +        public void onServiceConnected(Messenger m) { +            /** +             * This is never called through the proxy. +             */ +        } +    } + +    private static class Stub implements IStub { +        private IDownloaderClient mItf = null; +        private Class<?> mDownloaderServiceClass; +        private boolean mBound; +        private Messenger mServiceMessenger; +        private Context mContext; +        /** +         * Target we publish for clients to send messages to IncomingHandler. +         */ +        // -- GODOT start -- +        private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); +        final Messenger mMessenger = new Messenger(mMsgHandler); + +        private static class MessengerHandlerClient extends Handler { +            private final WeakReference<Stub> mDownloader; +            public MessengerHandlerClient(Stub downloader) { +                mDownloader = new WeakReference<>(downloader); +            } + +            @Override +            public void handleMessage(Message msg) { +                Stub downloader = mDownloader.get(); +                if (downloader != null) { +                    downloader.handleMessage(msg); +                } +            } +        } + +        private void handleMessage(Message msg) { +            switch (msg.what) { +                case MSG_ONDOWNLOADPROGRESS: +                    Bundle bun = msg.getData(); +                    if (null != mContext) { +                        bun.setClassLoader(mContext.getClassLoader()); +                        DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData() +                                                            .getParcelable(PARAM_PROGRESS); +                        mItf.onDownloadProgress(dpi); +                    } +                    break; +                case MSG_ONDOWNLOADSTATE_CHANGED: +                    mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); +                    break; +                case MSG_ONSERVICECONNECTED: +                    mItf.onServiceConnected( +                            (Messenger)msg.getData().getParcelable(PARAM_MESSENGER)); +                    break; +            } +        } +        // -- GODOT end -- + +        public Stub(IDownloaderClient itf, Class<?> downloaderService) { +            mItf = itf; +            mDownloaderServiceClass = downloaderService; +        } + +        /** +         * Class for interacting with the main interface of the service. +         */ +        private ServiceConnection mConnection = new ServiceConnection() { +            public void onServiceConnected(ComponentName className, IBinder service) { +                // This is called when the connection with the service has been +                // established, giving us the object we can use to +                // interact with the service. We are communicating with the +                // service using a Messenger, so here we get a client-side +                // representation of that from the raw IBinder object. +                mServiceMessenger = new Messenger(service); +                mItf.onServiceConnected( +                        mServiceMessenger); +            } + +            public void onServiceDisconnected(ComponentName className) { +                // This is called when the connection with the service has been +                // unexpectedly disconnected -- that is, its process crashed. +                mServiceMessenger = null; +            } +        }; + +        @Override +        public void connect(Context c) { +            mContext = c; +            Intent bindIntent = new Intent(c, mDownloaderServiceClass); +            bindIntent.putExtra(PARAM_MESSENGER, mMessenger); +            if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) { +                if ( Constants.LOGVV ) { +                    Log.d(Constants.TAG, "Service Unbound"); +                } +            } else { +                mBound = true; +            } + +        } + +        @Override +        public void disconnect(Context c) { +            if (mBound) { +                c.unbindService(mConnection); +                mBound = false; +            } +            mContext = null; +        } + +        @Override +        public Messenger getMessenger() { +            return mMessenger; +        } +    } + +    /** +     * Returns a proxy that will marshal calls to IDownloaderClient methods +     * +     * @param msg +     * @return +     */ +    public static IDownloaderClient CreateProxy(Messenger msg) { +        return new Proxy(msg); +    } + +    /** +     * Returns a stub object that, when connected, will listen for marshaled +     * {@link IDownloaderClient} methods and translate them into calls to the supplied +     * interface. +     * +     * @param itf An implementation of IDownloaderClient that will be called +     *            when remote method calls are unmarshaled. +     * @param downloaderService The class for your implementation of {@link +     * impl.DownloaderService}. +     * @return The {@link IStub} that allows you to connect to the service such that +     * your {@link IDownloaderClient} receives status updates. +     */ +    public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { +        return new Stub(itf, downloaderService); +    } + +    /** +     * Starts the download if necessary. This function starts a flow that does ` +     * many things. 1) Checks to see if the APK version has been checked and +     * the metadata database updated 2) If the APK version does not match, +     * checks the new LVL status to see if a new download is required 3) If the +     * APK version does match, then checks to see if the download(s) have been +     * completed 4) If the downloads have been completed, returns +     * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the +     * startup of an application to quickly ascertain if the application needs +     * to wait to hear about any updated APK expansion files. Note that this does +     * mean that the application MUST be run for the first time with a network +     * connection, even if Market delivers all of the files. +     * +     * @param context Your application Context. +     * @param notificationClient A PendingIntent to start the Activity in your application +     * that shows the download progress and which will also start the application when download +     * completes. +     * @param serviceClass the class of your {@link imp.DownloaderService} implementation +     * @return whether the service was started and the reason for starting the service. +     * Either {@link #NO_DOWNLOAD_REQUIRED}, {@link #LVL_CHECK_REQUIRED}, or {@link +     * #DOWNLOAD_REQUIRED}. +     * @throws NameNotFoundException +     */ +    public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, +            Class<?> serviceClass) +            throws NameNotFoundException { +        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, +                serviceClass); +    } + +    /** +     * This version assumes that the intent contains the pending intent as a parameter. This +     * is used for responding to alarms. +     * <p>The pending intent must be in an extra with the key {@link +     * impl.DownloaderService#EXTRA_PENDING_INTENT}. +     * +     * @param context +     * @param notificationClient +     * @param serviceClass the class of the service to start +     * @return +     * @throws NameNotFoundException +     */ +    public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, +            Class<?> serviceClass) +            throws NameNotFoundException { +        return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, +                serviceClass); +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java new file mode 100644 index 0000000000..3771d19c9b --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/DownloaderServiceMarshaller.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + +// -- GODOT start -- +import java.lang.ref.WeakReference; +// -- GODOT end -- + + +/** + * This class is used by the client activity to proxy requests to the Downloader + * Service. + * + * Most importantly, you must call {@link #CreateProxy} during the {@link + * IDownloaderClient#onServiceConnected} callback in your activity in order to instantiate + * an {@link IDownloaderService} object that you can then use to issue commands to the {@link + * DownloaderService} (such as to pause and resume downloads). + */ +public class DownloaderServiceMarshaller { + +    public static final int MSG_REQUEST_ABORT_DOWNLOAD = +            1; +    public static final int MSG_REQUEST_PAUSE_DOWNLOAD = +            2; +    public static final int MSG_SET_DOWNLOAD_FLAGS = +            3; +    public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = +            4; +    public static final int MSG_REQUEST_DOWNLOAD_STATE = +            5; +    public static final int MSG_REQUEST_CLIENT_UPDATE = +            6; + +    public static final String PARAMS_FLAGS = "flags"; +    public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; + +    private static class Proxy implements IDownloaderService { +        private Messenger mMsg; + +        private void send(int method, Bundle params) { +            Message m = Message.obtain(null, method); +            m.setData(params); +            try { +                mMsg.send(m); +            } catch (RemoteException e) { +                e.printStackTrace(); +            } +        } + +        public Proxy(Messenger msg) { +            mMsg = msg; +        } + +        @Override +        public void requestAbortDownload() { +            send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); +        } + +        @Override +        public void requestPauseDownload() { +            send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); +        } + +        @Override +        public void setDownloadFlags(int flags) { +            Bundle params = new Bundle(); +            params.putInt(PARAMS_FLAGS, flags); +            send(MSG_SET_DOWNLOAD_FLAGS, params); +        } + +        @Override +        public void requestContinueDownload() { +            send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); +        } + +        @Override +        public void requestDownloadStatus() { +            send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); +        } + +        @Override +        public void onClientUpdated(Messenger clientMessenger) { +            Bundle bundle = new Bundle(1); +            bundle.putParcelable(PARAM_MESSENGER, clientMessenger); +            send(MSG_REQUEST_CLIENT_UPDATE, bundle); +        } +    } + +    private static class Stub implements IStub { +        private IDownloaderService mItf = null; +        // -- GODOT start -- +        private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); +        final Messenger mMessenger = new Messenger(mMsgHandler); + +        private static class MessengerHandlerServer extends Handler { +            private final WeakReference<Stub> mDownloader; +            public MessengerHandlerServer(Stub downloader) { +                mDownloader = new WeakReference<>(downloader); +            } + +            @Override +            public void handleMessage(Message msg) { +                Stub downloader = mDownloader.get(); +                if (downloader != null) { +                    downloader.handleMessage(msg); +                } +            } +        } + +        private void handleMessage(Message msg) { +            switch (msg.what) { +                case MSG_REQUEST_ABORT_DOWNLOAD: +                    mItf.requestAbortDownload(); +                    break; +                case MSG_REQUEST_CONTINUE_DOWNLOAD: +                    mItf.requestContinueDownload(); +                    break; +                case MSG_REQUEST_PAUSE_DOWNLOAD: +                    mItf.requestPauseDownload(); +                    break; +                case MSG_SET_DOWNLOAD_FLAGS: +                    mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); +                    break; +                case MSG_REQUEST_DOWNLOAD_STATE: +                    mItf.requestDownloadStatus(); +                    break; +                case MSG_REQUEST_CLIENT_UPDATE: +                    mItf.onClientUpdated((Messenger)msg.getData().getParcelable( +                            PARAM_MESSENGER)); +                    break; +            } +        } +        // -- GODOT end -- + +        public Stub(IDownloaderService itf) { +            mItf = itf; +        } + +        @Override +        public Messenger getMessenger() { +            return mMessenger; +        } + +        @Override +        public void connect(Context c) { + +        } + +        @Override +        public void disconnect(Context c) { + +        } +    } + +    /** +     * Returns a proxy that will marshall calls to IDownloaderService methods +     * +     * @param ctx +     * @return +     */ +    public static IDownloaderService CreateProxy(Messenger msg) { +        return new Proxy(msg); +    } + +    /** +     * Returns a stub object that, when connected, will listen for marshalled +     * IDownloaderService methods and translate them into calls to the supplied +     * interface. +     * +     * @param itf An implementation of IDownloaderService that will be called +     *            when remote method calls are unmarshalled. +     * @return +     */ +    public static IStub CreateStub(IDownloaderService itf) { +        return new Stub(itf); +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java new file mode 100644 index 0000000000..2a72c9818d --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/Helpers.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.os.SystemClock; +import android.util.Log; + +// -- GODOT start -- +//import com.android.vending.expansion.downloader.R; +import org.godotengine.godot.R; +// -- GODOT end -- + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Some helper functions for the download manager + */ +public class Helpers { + +    public static Random sRandom = new Random(SystemClock.uptimeMillis()); + +    /** Regex used to parse content-disposition headers */ +    private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern +            .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + +    private Helpers() { +    } + +    /* +     * Parse the Content-Disposition HTTP Header. The format of the header is defined here: +     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for +     * content that is going to be downloaded to the file system. We only support the attachment +     * type. +     */ +    static String parseContentDisposition(String contentDisposition) { +        try { +            Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); +            if (m.find()) { +                return m.group(1); +            } +        } catch (IllegalStateException ex) { +            // This function is defined as returning null when it can't parse +            // the header +        } +        return null; +    } + +    /** +     * @return the root of the filesystem containing the given path +     */ +    public static File getFilesystemRoot(String path) { +        File cache = Environment.getDownloadCacheDirectory(); +        if (path.startsWith(cache.getPath())) { +            return cache; +        } +        File external = Environment.getExternalStorageDirectory(); +        if (path.startsWith(external.getPath())) { +            return external; +        } +        throw new IllegalArgumentException( +                "Cannot determine filesystem root for " + path); +    } + +    public static boolean isExternalMediaMounted() { +        if (!Environment.getExternalStorageState().equals( +                Environment.MEDIA_MOUNTED)) { +            // No SD card found. +            if (Constants.LOGVV) { +                Log.d(Constants.TAG, "no external storage"); +            } +            return false; +        } +        return true; +    } + +    /** +     * @return the number of bytes available on the filesystem rooted at the given File +     */ +    public static long getAvailableBytes(File root) { +        StatFs stat = new StatFs(root.getPath()); +        // put a bit of margin (in case creating the file grows the system by a +        // few blocks) +        long availableBlocks = (long) stat.getAvailableBlocks() - 4; +        return stat.getBlockSize() * availableBlocks; +    } + +    /** +     * Checks whether the filename looks legitimate +     */ +    public static boolean isFilenameValid(String filename) { +        filename = filename.replaceFirst("/+", "/"); // normalize leading +                                                     // slashes +        return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) +                || filename.startsWith(Environment.getExternalStorageDirectory().toString()); +    } + +    /* +     * Delete the given file from device +     */ +    /* package */static void deleteFile(String path) { +        try { +            File file = new File(path); +            file.delete(); +        } catch (Exception e) { +            Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); +        } +    } + +    /** +     * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total +     * file size, but given what we know about the expected ranges of file sizes for APK expansion +     * files, it's probably not necessary. +     * +     * @param overallProgress +     * @param overallTotal +     * @return +     */ + +    static public String getDownloadProgressString(long overallProgress, long overallTotal) { +        if (overallTotal == 0) { +            if (Constants.LOGVV) { +                Log.e(Constants.TAG, "Notification called when total is zero"); +            } +            return ""; +        } +        // -- GODOT start -- +        return String.format(Locale.ENGLISH, "%.2f", +                (float) overallProgress / (1024.0f * 1024.0f)) +                + "MB /" + +                String.format(Locale.ENGLISH, "%.2f", (float) overallTotal / +                        (1024.0f * 1024.0f)) +                + "MB"; +        // -- GODOT end -- +    } + +    /** +     * Adds a percentile to getDownloadProgressString. +     * +     * @param overallProgress +     * @param overallTotal +     * @return +     */ +    static public String getDownloadProgressStringNotification(long overallProgress, +            long overallTotal) { +        if (overallTotal == 0) { +            if (Constants.LOGVV) { +                Log.e(Constants.TAG, "Notification called when total is zero"); +            } +            return ""; +        } +        return getDownloadProgressString(overallProgress, overallTotal) + " (" + +                getDownloadProgressPercent(overallProgress, overallTotal) + ")"; +    } + +    public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { +        if (overallTotal == 0) { +            if (Constants.LOGVV) { +                Log.e(Constants.TAG, "Notification called when total is zero"); +            } +            return ""; +        } +        return Long.toString(overallProgress * 100 / overallTotal) + "%"; +    } + +    public static String getSpeedString(float bytesPerMillisecond) { +        // -- GODOT start -- +        return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); +        // -- GODOT end -- +    } + +    public static String getTimeRemaining(long durationInMilliseconds) { +        SimpleDateFormat sdf; +        if (durationInMilliseconds > 1000 * 60 * 60) { +            sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); +        } else { +            sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); +        } +        return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); +    } + +    /** +     * Returns the file name (without full path) for an Expansion APK file from the given context. +     * +     * @param c the context +     * @param mainFile true for main file, false for patch file +     * @param versionCode the version of the file +     * @return String the file name of the expansion file +     */ +    public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { +        return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; +    } + +    /** +     * Returns the filename (where the file should be saved) from info about a download +     */ +    static public String generateSaveFileName(Context c, String fileName) { +        String path = getSaveFilePath(c) +                + File.separator + fileName; +        return path; +    } + +    @TargetApi(Build.VERSION_CODES.HONEYCOMB) +    static public String getSaveFilePath(Context c) { +        // This technically existed since Honeycomb, but it is critical +        // on KitKat and greater versions since it will create the +        // directory if needed +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +            return c.getObbDir().toString(); +        } else { +            File root = Environment.getExternalStorageDirectory(); +            String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); +            return path; +        } +    } + +    /** +     * Helper function to ascertain the existence of a file and return true/false appropriately +     * +     * @param c the app/activity/service context +     * @param fileName the name (sans path) of the file to query +     * @param fileSize the size that the file must match +     * @param deleteFileOnMismatch if the file sizes do not match, delete the file +     * @return true if it does exist, false otherwise +     */ +    static public boolean doesFileExist(Context c, String fileName, long fileSize, +            boolean deleteFileOnMismatch) { +        // the file may have been delivered by Play --- let's make sure +        // it's the size we expect +        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); +        if (fileForNewFile.exists()) { +            if (fileForNewFile.length() == fileSize) { +                return true; +            } +            if (deleteFileOnMismatch) { +                // delete the file --- we won't be able to resume +                // because we cannot confirm the integrity of the file +                fileForNewFile.delete(); +            } +        } +        return false; +    } + +    public static final int FS_READABLE = 0; +    public static final int FS_DOES_NOT_EXIST = 1; +    public static final int FS_CANNOT_READ = 2; + +    /** +     * Helper function to ascertain whether a file can be read. +     * +     * @param c the app/activity/service context +     * @param fileName the name (sans path) of the file to query +     * @return true if it does exist, false otherwise +     */ +    static public int getFileStatus(Context c, String fileName) { +        // the file may have been delivered by Play --- let's make sure +        // it's the size we expect +        File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); +        int returnValue; +        if (fileForNewFile.exists()) { +            if (fileForNewFile.canRead()) { +                returnValue = FS_READABLE; +            } else { +                returnValue = FS_CANNOT_READ; +            } +        } else { +            returnValue = FS_DOES_NOT_EXIST; +        } +        return returnValue; +    } + +    /** +     * Helper function to ascertain whether the application has the correct access to the OBB +     * directory to allow an OBB file to be written. +     *  +     * @param c the app/activity/service context +     * @return true if the application can write an OBB file, false otherwise +     */ +    static public boolean canWriteOBBFile(Context c) { +        String path = getSaveFilePath(c); +        File fileForNewFile = new File(path); +        boolean canWrite; +        if (fileForNewFile.exists()) { +            canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite(); +        } else { +            canWrite = fileForNewFile.mkdirs(); +        } +        return canWrite; +    } + +    /** +     * Converts download states that are returned by the +     * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful +     * if using the state strings built into the library to display user messages. +     *  +     * @param state One of the STATE_* constants from {@link IDownloaderClient}. +     * @return string resource ID for the corresponding string. +     */ +    static public int getDownloaderStringResourceIDFromState(int state) { +        switch (state) { +            case IDownloaderClient.STATE_IDLE: +                return R.string.state_idle; +            case IDownloaderClient.STATE_FETCHING_URL: +                return R.string.state_fetching_url; +            case IDownloaderClient.STATE_CONNECTING: +                return R.string.state_connecting; +            case IDownloaderClient.STATE_DOWNLOADING: +                return R.string.state_downloading; +            case IDownloaderClient.STATE_COMPLETED: +                return R.string.state_completed; +            case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: +                return R.string.state_paused_network_unavailable; +            case IDownloaderClient.STATE_PAUSED_BY_REQUEST: +                return R.string.state_paused_by_request; +            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: +                return R.string.state_paused_wifi_disabled; +            case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: +                return R.string.state_paused_wifi_unavailable; +            case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: +                return R.string.state_paused_wifi_disabled; +            case IDownloaderClient.STATE_PAUSED_NEED_WIFI: +                return R.string.state_paused_wifi_unavailable; +            case IDownloaderClient.STATE_PAUSED_ROAMING: +                return R.string.state_paused_roaming; +            case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: +                return R.string.state_paused_network_setup_failure; +            case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: +                return R.string.state_paused_sdcard_unavailable; +            case IDownloaderClient.STATE_FAILED_UNLICENSED: +                return R.string.state_failed_unlicensed; +            case IDownloaderClient.STATE_FAILED_FETCHING_URL: +                return R.string.state_failed_fetching_url; +            case IDownloaderClient.STATE_FAILED_SDCARD_FULL: +                return R.string.state_failed_sdcard_full; +            case IDownloaderClient.STATE_FAILED_CANCELED: +                return R.string.state_failed_cancelled; +            default: +                return R.string.state_unknown; +        } +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java new file mode 100644 index 0000000000..cef3794701 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderClient.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.os.Messenger; + +/** + * This interface should be implemented by the client activity for the + * downloader. It is used to pass status from the service to the client. + */ +public interface IDownloaderClient { +    static final int STATE_IDLE = 1; +    static final int STATE_FETCHING_URL = 2; +    static final int STATE_CONNECTING = 3; +    static final int STATE_DOWNLOADING = 4; +    static final int STATE_COMPLETED = 5; + +    static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; +    static final int STATE_PAUSED_BY_REQUEST = 7; + +    /** +     * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and +     * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and +     * cellular permission will restart the service. Wi-Fi disabled means that +     * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the +     * other case Wi-Fi is enabled but not available. +     */ +    static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; +    static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; + +    /** +     * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that +     * Wi-Fi is unavailable and cellular permission will NOT restart the +     * service. Wi-Fi disabled means that the Wi-Fi manager is returning that +     * Wi-Fi is not enabled, while in the other case Wi-Fi is enabled but not +     * available. +     * <p> +     * The service does not return these values. We recommend that app +     * developers with very large payloads do not allow these payloads to be +     * downloaded over cellular connections. +     */ +    static final int STATE_PAUSED_WIFI_DISABLED = 10; +    static final int STATE_PAUSED_NEED_WIFI = 11; + +    static final int STATE_PAUSED_ROAMING = 12; + +    /** +     * Scary case. We were on a network that redirected us to another website +     * that delivered us the wrong file. +     */ +    static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; + +    static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; + +    static final int STATE_FAILED_UNLICENSED = 15; +    static final int STATE_FAILED_FETCHING_URL = 16; +    static final int STATE_FAILED_SDCARD_FULL = 17; +    static final int STATE_FAILED_CANCELED = 18; + +    static final int STATE_FAILED = 19; + +    /** +     * Called internally by the stub when the service is bound to the client. +     * <p> +     * Critical implementation detail. In onServiceConnected we create the +     * remote service and marshaler. This is how we pass the client information +     * back to the service so the client can be properly notified of changes. We +     * must do this every time we reconnect to the service. +     * <p> +     * That is, when you receive this callback, you should call +     * {@link DownloaderServiceMarshaller#CreateProxy} to instantiate a member +     * instance of {@link IDownloaderService}, then call +     * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved +     * from your {@link IStub} proxy object. +     * +     * @param m the service Messenger. This Messenger is used to call the +     *            service API from the client. +     */ +    void onServiceConnected(Messenger m); + +    /** +     * Called when the download state changes. Depending on the state, there may +     * be user requests. The service is free to change the download state in the +     * middle of a user request, so the client should be able to handle this. +     * <p> +     * The Downloader Library includes a collection of string resources that +     * correspond to each of the states, which you can use to provide users a +     * useful message based on the state provided in this callback. To fetch the +     * appropriate string for a state, call +     * {@link Helpers#getDownloaderStringResourceIDFromState}. +     * <p> +     * What this means to the developer: The application has gotten a message +     * that the download has paused due to lack of WiFi. The developer should +     * then show UI asking the user if they want to enable downloading over +     * cellular connections with appropriate warnings. If the application +     * suddenly starts downloading, the application should revert to showing the +     * progress again, rather than leaving up the download over cellular UI up. +     * +     * @param newState one of the STATE_* values defined in IDownloaderClient +     */ +    void onDownloadStateChanged(int newState); + +    /** +     * Shows the download progress. This is intended to be used to fill out a +     * client UI. This progress should only be shown in a few states such as +     * STATE_DOWNLOADING. +     * +     * @param progress the DownloadProgressInfo object containing the current +     *            progress of all downloads. +     */ +    void onDownloadProgress(DownloadProgressInfo progress); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java new file mode 100644 index 0000000000..4de9de0c62 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IDownloaderService.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import com.google.android.vending.expansion.downloader.impl.DownloaderService; +import android.os.Messenger; + +/** + * This interface is implemented by the DownloaderService and by the + * DownloaderServiceMarshaller. It contains functions to control the service. + * When a client binds to the service, it must call the onClientUpdated + * function. + * <p> + * You can acquire a proxy that implements this interface for your service by + * calling {@link DownloaderServiceMarshaller#CreateProxy} during the + * {@link IDownloaderClient#onServiceConnected} callback. At which point, you + * should immediately call {@link #onClientUpdated}. + */ +public interface IDownloaderService { +    /** +     * Set this flag in response to the +     * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then +     * call RequestContinueDownload to resume a download +     */ +    public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; + +    /** +     * Request that the service abort the current download. The service should +     * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. +     */ +    void requestAbortDownload(); + +    /** +     * Request that the service pause the current download. The service should +     * respond by changing the state to +     * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. +     */ +    void requestPauseDownload(); + +    /** +     * Request that the service continue a paused download, when in any paused +     * or failed state, including +     * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. +     */ +    void requestContinueDownload(); + +    /** +     * Set the flags for this download (e.g. +     * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). +     * +     * @param flags +     */ +    void setDownloadFlags(int flags); + +    /** +     * Requests that the download status be sent to the client. +     */ +    void requestDownloadStatus(); + +    /** +     * Call this when you get {@link +     * IDownloaderClient.onServiceConnected(Messenger m)} from the +     * DownloaderClient to register the client with the service. It will +     * automatically send the current status to the client. +     * +     * @param clientMessenger +     */ +    void onClientUpdated(Messenger clientMessenger); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java new file mode 100644 index 0000000000..d5bc3a843e --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/IStub.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.content.Context; +import android.os.Messenger; + +/** + * This is the interface that is used to connect/disconnect from the downloader + * service. + * <p> + * You should get a proxy object that implements this interface by calling + * {@link DownloaderClientMarshaller#CreateStub} in your activity when the + * downloader service starts. Then, call {@link #connect} during your activity's + * onResume() and call {@link #disconnect} during onStop(). + * <p> + * Then during the {@link IDownloaderClient#onServiceConnected} callback, you + * should call {@link #getMessenger} to pass the stub's Messenger object to + * {@link IDownloaderService#onClientUpdated}. + */ +public interface IStub { +    Messenger getMessenger(); + +    void connect(Context c); + +    void disconnect(Context c); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java new file mode 100644 index 0000000000..a0e1165cc4 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/SystemFacade.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; +import android.util.Log; + +// -- GODOT start -- +import android.annotation.SuppressLint; +// -- GODOT end -- + +/** + * Contains useful helper functions, typically tied to the application context. + */ +class SystemFacade { +    private Context mContext; +    private NotificationManager mNotificationManager; + +    public SystemFacade(Context context) { +        mContext = context; +        mNotificationManager = (NotificationManager) +                mContext.getSystemService(Context.NOTIFICATION_SERVICE); +    } + +    public long currentTimeMillis() { +        return System.currentTimeMillis(); +    } + +    public Integer getActiveNetworkType() { +        ConnectivityManager connectivity = +                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); +        if (connectivity == null) { +            Log.w(Constants.TAG, "couldn't get connectivity manager"); +            return null; +        } + +        @SuppressLint("MissingPermission") +        NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); +        if (activeInfo == null) { +            if (Constants.LOGVV) { +                Log.v(Constants.TAG, "network is not available"); +            } +            return null; +        } +        return activeInfo.getType(); +    } + +    public boolean isNetworkRoaming() { +        ConnectivityManager connectivity = +                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); +        if (connectivity == null) { +            Log.w(Constants.TAG, "couldn't get connectivity manager"); +            return false; +        } + +        @SuppressLint("MissingPermission") +        NetworkInfo info = connectivity.getActiveNetworkInfo(); +        boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); +        TelephonyManager tm = (TelephonyManager) mContext +                .getSystemService(Context.TELEPHONY_SERVICE); +        if (null == tm) { +            Log.w(Constants.TAG, "couldn't get telephony manager"); +            return false; +        } +        boolean isRoaming = isMobile && tm.isNetworkRoaming(); +        if (Constants.LOGVV && isRoaming) { +            Log.v(Constants.TAG, "network is roaming"); +        } +        return isRoaming; +    } + +    public Long getMaxBytesOverMobile() { +        return (long) Integer.MAX_VALUE; +    } + +    public Long getRecommendedMaxBytesOverMobile() { +        return 2097152L; +    } + +    public void sendBroadcast(Intent intent) { +        mContext.sendBroadcast(intent); +    } + +    public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { +        return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; +    } + +    public void postNotification(long id, Notification notification) { +        /** +         * TODO: The system notification manager takes ints, not longs, as IDs, +         * but the download manager uses IDs take straight from the database, +         * which are longs. This will have to be dealt with at some point. +         */ +        mNotificationManager.notify((int) id, notification); +    } + +    public void cancelNotification(long id) { +        mNotificationManager.cancel((int) id); +    } + +    public void cancelAllNotifications() { +        mNotificationManager.cancelAll(); +    } + +    public void startThread(Thread thread) { +        thread.start(); +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java new file mode 100644 index 0000000000..3ccc191c60 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/CustomIntentService.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +/** + * This service differs from IntentService in a few minor ways/ It will not + * auto-stop itself after the intent is handled unless the target returns "true" + * in should stop. Since the goal of this service is to handle a single kind of + * intent, it does not queue up batches of intents of the same type. + */ +public abstract class CustomIntentService extends Service { +    private String mName; +    private boolean mRedelivery; +    private volatile ServiceHandler mServiceHandler; +    private volatile Looper mServiceLooper; +    private static final String LOG_TAG = "CustomIntentService"; +    private static final int WHAT_MESSAGE = -10; + +    public CustomIntentService(String paramString) { +        this.mName = paramString; +    } + +    @Override +    public IBinder onBind(Intent paramIntent) { +        return null; +    } + +    @Override +    public void onCreate() { +        super.onCreate(); +        HandlerThread localHandlerThread = new HandlerThread("IntentService[" +                + this.mName + "]"); +        localHandlerThread.start(); +        this.mServiceLooper = localHandlerThread.getLooper(); +        this.mServiceHandler = new ServiceHandler(this.mServiceLooper); +    } + +    @Override +    public void onDestroy() { +        Thread localThread = this.mServiceLooper.getThread(); +        if ((localThread != null) && (localThread.isAlive())) { +            localThread.interrupt(); +        } +        this.mServiceLooper.quit(); +        Log.d(LOG_TAG, "onDestroy"); +    } + +    protected abstract void onHandleIntent(Intent paramIntent); + +    protected abstract boolean shouldStop(); + +    @Override +    public void onStart(Intent paramIntent, int startId) { +        if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { +            Message localMessage = this.mServiceHandler.obtainMessage(); +            localMessage.arg1 = startId; +            localMessage.obj = paramIntent; +            localMessage.what = WHAT_MESSAGE; +            this.mServiceHandler.sendMessage(localMessage); +        } +    } + +    @Override +    public int onStartCommand(Intent paramIntent, int flags, int startId) { +        onStart(paramIntent, startId); +        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; +    } + +    public void setIntentRedelivery(boolean enabled) { +        this.mRedelivery = enabled; +    } + +    private final class ServiceHandler extends Handler { +        public ServiceHandler(Looper looper) { +            super(looper); +        } + +        @Override +        public void handleMessage(Message paramMessage) { +            CustomIntentService.this +                    .onHandleIntent((Intent) paramMessage.obj); +            if (shouldStop()) { +                Log.d(LOG_TAG, "stopSelf"); +                CustomIntentService.this.stopSelf(paramMessage.arg1); +                Log.d(LOG_TAG, "afterStopSelf"); +            } +        } +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java new file mode 100644 index 0000000000..45111b16a3 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadInfo.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; + +import android.util.Log; + +/** + * Representation of information about an individual download from the database. + */ +public class DownloadInfo { +    public String mUri; +    public final int mIndex; +    public final String mFileName; +    public String mETag; +    public long mTotalBytes; +    public long mCurrentBytes; +    public long mLastMod; +    public int mStatus; +    public int mControl; +    public int mNumFailed; +    public int mRetryAfter; +    public int mRedirectCount; + +    boolean mInitialized; + +    public int mFuzz; + +    public DownloadInfo(int index, String fileName, String pkg) { +        mFuzz = Helpers.sRandom.nextInt(1001); +        mFileName = fileName; +        mIndex = index; +    } + +    public void resetDownload() { +        mCurrentBytes = 0; +        mETag = ""; +        mLastMod = 0; +        mStatus = 0; +        mControl = 0; +        mNumFailed = 0; +        mRetryAfter = 0; +        mRedirectCount = 0; +    } + +    /** +     * Returns the time when a download should be restarted. +     */ +    public long restartTime(long now) { +        if (mNumFailed == 0) { +            return now; +        } +        if (mRetryAfter > 0) { +            return mLastMod + mRetryAfter; +        } +        return mLastMod + +                Constants.RETRY_FIRST_DELAY * +                (1000 + mFuzz) * (1 << (mNumFailed - 1)); +    } + +    public void logVerboseInfo() { +        Log.v(Constants.TAG, "Service adding new entry"); +        Log.v(Constants.TAG, "FILENAME: " + mFileName); +        Log.v(Constants.TAG, "URI     : " + mUri); +        Log.v(Constants.TAG, "FILENAME: " + mFileName); +        Log.v(Constants.TAG, "CONTROL : " + mControl); +        Log.v(Constants.TAG, "STATUS  : " + mStatus); +        Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); +        Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); +        Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); +        Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); +        Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes); +        Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); +        Log.v(Constants.TAG, "ETAG    : " + mETag); +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java new file mode 100644 index 0000000000..0abaf2e052 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadNotification.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +// -- GODOT start -- +//import com.android.vending.expansion.downloader.R; +import org.godotengine.godot.R; +// -- GODOT end -- + +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build; +import android.os.Messenger; +import android.support.v4.app.NotificationCompat; + +/** + * This class handles displaying the notification associated with the download + * queue going on in the download manager. It handles multiple status types; + * Some require user interaction and some do not. Some of the user interactions + * may be transient. (for example: the user is queried to continue the download + * on 3G when it started on WiFi, but then the phone locks onto WiFi again so + * the prompt automatically goes away) + * <p/> + * The application interface for the downloader also needs to understand and + * handle these transient states. + */ +public class DownloadNotification implements IDownloaderClient { + +    private int mState; +    private final Context mContext; +    private final NotificationManager mNotificationManager; +    private CharSequence mCurrentTitle; + +    private IDownloaderClient mClientProxy; +    private NotificationCompat.Builder mActiveDownloadBuilder; +    private NotificationCompat.Builder mBuilder; +    private NotificationCompat.Builder mCurrentBuilder; +    private CharSequence mLabel; +    private String mCurrentText; +    private DownloadProgressInfo mProgressInfo; +    private PendingIntent mContentIntent; + +    static final String LOGTAG = "DownloadNotification"; +    static final int NOTIFICATION_ID = LOGTAG.hashCode(); + +    public PendingIntent getClientIntent() { +        return mContentIntent; +    } + +    public void setClientIntent(PendingIntent clientIntent) { +        this.mBuilder.setContentIntent(clientIntent); +        this.mActiveDownloadBuilder.setContentIntent(clientIntent); +        this.mContentIntent = clientIntent; +    } + +    public void resendState() { +        if (null != mClientProxy) { +            mClientProxy.onDownloadStateChanged(mState); +        } +    } + +    @Override +    public void onDownloadStateChanged(int newState) { +        if (null != mClientProxy) { +            mClientProxy.onDownloadStateChanged(newState); +        } +        if (newState != mState) { +            mState = newState; +            if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) { +                return; +            } +            int stringDownloadID; +            int iconResource; +            boolean ongoingEvent; + +            // get the new title string and paused text +            switch (newState) { +                case 0: +                    iconResource = android.R.drawable.stat_sys_warning; +                    stringDownloadID = R.string.state_unknown; +                    ongoingEvent = false; +                    break; + +                case IDownloaderClient.STATE_DOWNLOADING: +                    iconResource = android.R.drawable.stat_sys_download; +                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); +                    ongoingEvent = true; +                    break; + +                case IDownloaderClient.STATE_FETCHING_URL: +                case IDownloaderClient.STATE_CONNECTING: +                    iconResource = android.R.drawable.stat_sys_download_done; +                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); +                    ongoingEvent = true; +                    break; + +                case IDownloaderClient.STATE_COMPLETED: +                case IDownloaderClient.STATE_PAUSED_BY_REQUEST: +                    iconResource = android.R.drawable.stat_sys_download_done; +                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); +                    ongoingEvent = false; +                    break; + +                case IDownloaderClient.STATE_FAILED: +                case IDownloaderClient.STATE_FAILED_CANCELED: +                case IDownloaderClient.STATE_FAILED_FETCHING_URL: +                case IDownloaderClient.STATE_FAILED_SDCARD_FULL: +                case IDownloaderClient.STATE_FAILED_UNLICENSED: +                    iconResource = android.R.drawable.stat_sys_warning; +                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); +                    ongoingEvent = false; +                    break; + +                default: +                    iconResource = android.R.drawable.stat_sys_warning; +                    stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); +                    ongoingEvent = true; +                    break; +            } + +            mCurrentText = mContext.getString(stringDownloadID); +            mCurrentTitle = mLabel; +            mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText); +            mCurrentBuilder.setSmallIcon(iconResource); +            mCurrentBuilder.setContentTitle(mCurrentTitle); +            mCurrentBuilder.setContentText(mCurrentText); +            if (ongoingEvent) { +                mCurrentBuilder.setOngoing(true); +            } else { +                mCurrentBuilder.setOngoing(false); +                mCurrentBuilder.setAutoCancel(true); +            } +            mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); +        } +    } + +    @Override +    public void onDownloadProgress(DownloadProgressInfo progress) { +        mProgressInfo = progress; +        if (null != mClientProxy) { +            mClientProxy.onDownloadProgress(progress); +        } +        if (progress.mOverallTotal <= 0) { +            // we just show the text +            mBuilder.setTicker(mCurrentTitle); +            mBuilder.setSmallIcon(android.R.drawable.stat_sys_download); +            mBuilder.setContentTitle(mCurrentTitle); +            mBuilder.setContentText(mCurrentText); +            mCurrentBuilder = mBuilder; +        } else { +            mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false); +            mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); +            mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download); +            mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText); +            mActiveDownloadBuilder.setContentTitle(mLabel); +            mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification, +                    Helpers.getTimeRemaining(progress.mTimeRemaining))); +            mCurrentBuilder = mActiveDownloadBuilder; +        } +        mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); +    } + +    /** +     * Called in response to onClientUpdated. Creates a new proxy and notifies +     * it of the current state. +     * +     * @param msg the client Messenger to notify +     */ +    public void setMessenger(Messenger msg) { +        mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); +        if (null != mProgressInfo) { +            mClientProxy.onDownloadProgress(mProgressInfo); +        } +        if (mState != -1) { +            mClientProxy.onDownloadStateChanged(mState); +        } +    } + +    /** +     * Constructor +     * +     * @param ctx The context to use to obtain access to the Notification +     *            Service +     */ +    DownloadNotification(Context ctx, CharSequence applicationLabel) { +        mState = -1; +        mContext = ctx; +        mLabel = applicationLabel; +        mNotificationManager = (NotificationManager) +                mContext.getSystemService(Context.NOTIFICATION_SERVICE); +        mActiveDownloadBuilder = new NotificationCompat.Builder(ctx); +        mBuilder = new NotificationCompat.Builder(ctx); + +        // Set Notification category and priorities to something that makes sense for a long +        // lived background task. +        mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW); +        mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); + +        mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); +        mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); + +        mCurrentBuilder = mBuilder; +    } + +    @Override +    public void onServiceConnected(Messenger m) { +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java new file mode 100644 index 0000000000..c114b8a64a --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadThread.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; + +import android.content.Context; +import android.os.PowerManager; +import android.os.Process; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SyncFailedException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; + +/** + * Runs an actual download + */ +public class DownloadThread { + +    private Context mContext; +    private DownloadInfo mInfo; +    private DownloaderService mService; +    private final DownloadsDB mDB; +    private final DownloadNotification mNotification; +    private String mUserAgent; + +    public DownloadThread(DownloadInfo info, DownloaderService service, +            DownloadNotification notification) { +        mContext = service; +        mInfo = info; +        mService = service; +        mNotification = notification; +        mDB = DownloadsDB.getDB(service); +        mUserAgent = "APKXDL (Linux; U; Android " + android.os.Build.VERSION.RELEASE + ";" +                + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE + "/" +                + android.os.Build.ID + ")" + +                service.getPackageName(); +    } + +    /** +     * Returns the default user agent +     */ +    private String userAgent() { +        return mUserAgent; +    } + +    /** +     * State for the entire run() method. +     */ +    private static class State { +        public String mFilename; +        public FileOutputStream mStream; +        public boolean mCountRetry = false; +        public int mRetryAfter = 0; +        public int mRedirectCount = 0; +        public String mNewUri; +        public boolean mGotData = false; +        public String mRequestUri; + +        public State(DownloadInfo info, DownloaderService service) { +            mRedirectCount = info.mRedirectCount; +            mRequestUri = info.mUri; +            mFilename = service.generateTempSaveFileName(info.mFileName); +        } +    } + +    /** +     * State within executeDownload() +     */ +    private static class InnerState { +        public int mBytesSoFar = 0; +        public int mBytesThisSession = 0; +        public String mHeaderETag; +        public boolean mContinuingDownload = false; +        public String mHeaderContentLength; +        public String mHeaderContentDisposition; +        public String mHeaderContentLocation; +        public int mBytesNotified = 0; +        public long mTimeLastNotification = 0; +    } + +    /** +     * Raised from methods called by run() to indicate that the current request +     * should be stopped immediately. Note the message passed to this exception +     * will be logged and therefore must be guaranteed not to contain any PII, +     * meaning it generally can't include any information about the request URI, +     * headers, or destination filename. +     */ +    private class StopRequest extends Throwable { +    	 +        private static final long serialVersionUID = 6338592678988347973L; +        public int mFinalStatus; + +        public StopRequest(int finalStatus, String message) { +            super(message); +            mFinalStatus = finalStatus; +        } + +        public StopRequest(int finalStatus, String message, Throwable throwable) { +            super(message, throwable); +            mFinalStatus = finalStatus; +        } +    } + +    /** +     * Raised from methods called by executeDownload() to indicate that the +     * download should be retried immediately. +     */ +    private class RetryDownload extends Throwable { + +        private static final long serialVersionUID = 6196036036517540229L; +    } + +    /** +     * Executes the download in a separate thread +     */ +    public void run() { +        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + +        State state = new State(mInfo, mService); +        PowerManager.WakeLock wakeLock = null; +        int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; + +        try { +            PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); +            // -- GODOT start -- +            //wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); +            //wakeLock.acquire(); +            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock"); +            wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/); +            // -- GODOT end -- + +            if (Constants.LOGV) { +                Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); +                Log.v(Constants.TAG, "  at " + mInfo.mUri); +            } + +            boolean finished = false; +            while (!finished) { +                if (Constants.LOGV) { +                    Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName); +                    Log.v(Constants.TAG, "  at " + mInfo.mUri); +                } +                // Set or unset proxy, which may have changed since last GET +                // request. +                // setDefaultProxy() supports null as proxy parameter. +                URL url = new URL(state.mRequestUri); +                HttpURLConnection request = (HttpURLConnection)url.openConnection(); +                request.setRequestProperty("User-Agent", userAgent()); +                try { +                    executeDownload(state, request); +                    finished = true; +                } catch (RetryDownload exc) { +                    // fall through +                } finally { +                    request.disconnect(); +                    request = null; +                } +            } + +            if (Constants.LOGV) { +                Log.v(Constants.TAG, "download completed for " + mInfo.mFileName); +                Log.v(Constants.TAG, "  at " + mInfo.mUri); +            } +            finalizeDestinationFile(state); +            finalStatus = DownloaderService.STATUS_SUCCESS; +        } catch (StopRequest error) { +            // remove the cause before printing, in case it contains PII +            Log.w(Constants.TAG, +                    "Aborting request for download " + mInfo.mFileName + ": " + error.getMessage()); +            error.printStackTrace(); +            finalStatus = error.mFinalStatus; +            // fall through to finally block +        } catch (Throwable ex) { // sometimes the socket code throws unchecked +                                 // exceptions +            Log.w(Constants.TAG, "Exception for " + mInfo.mFileName + ": " + ex); +            finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR; +            // falls through to the code that reports an error +        } finally { +            if (wakeLock != null) { +                wakeLock.release(); +                wakeLock = null; +            } +            cleanupDestination(state, finalStatus); +            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, +                    state.mRedirectCount, state.mGotData, state.mFilename); +        } +    } + +    /** +     * Fully execute a single download request - setup and send the request, +     * handle the response, and transfer the data to the destination file. +     */ +    private void executeDownload(State state, HttpURLConnection request) +            throws StopRequest, RetryDownload { +        InnerState innerState = new InnerState(); +        byte data[] = new byte[Constants.BUFFER_SIZE]; + +        checkPausedOrCanceled(state); + +        setupDestinationFile(state, innerState); +        addRequestHeaders(innerState, request); + +        // check just before sending the request to avoid using an invalid +        // connection at all +        checkConnectivity(state); + +        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_CONNECTING); +        int responseCode = sendRequest(state, request); +        handleExceptionalStatus(state, innerState, request, responseCode); + +        if (Constants.LOGV) { +            Log.v(Constants.TAG, "received response for " + mInfo.mUri); +        } + +        processResponseHeaders(state, innerState, request); +        InputStream entityStream = openResponseEntity(state, request); +        mNotification.onDownloadStateChanged(IDownloaderClient.STATE_DOWNLOADING); +        transferData(state, innerState, data, entityStream); +    } + +    /** +     * Check if current connectivity is valid for this request. +     */ +    private void checkConnectivity(State state) throws StopRequest { +        switch (mService.getNetworkAvailabilityState(mDB)) { +            case DownloaderService.NETWORK_OK: +                return; +            case DownloaderService.NETWORK_NO_CONNECTION: +                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, +                        "waiting for network to return"); +            case DownloaderService.NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: +                throw new StopRequest( +                        DownloaderService.STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION, +                        "waiting for wifi or for download over cellular to be authorized"); +            case DownloaderService.NETWORK_CANNOT_USE_ROAMING: +                throw new StopRequest(DownloaderService.STATUS_WAITING_FOR_NETWORK, +                        "roaming is not allowed"); +            case DownloaderService.NETWORK_UNUSABLE_DUE_TO_SIZE: +                throw new StopRequest(DownloaderService.STATUS_QUEUED_FOR_WIFI, "waiting for wifi"); +        } +    } + +    /** +     * Transfer as much data as possible from the HTTP response to the +     * destination file. +     * +     * @param data buffer to use to read data +     * @param entityStream stream for reading the HTTP response entity +     */ +    private void transferData(State state, InnerState innerState, byte[] data, +            InputStream entityStream) throws StopRequest { +        for (;;) { +            int bytesRead = readFromResponse(state, innerState, data, entityStream); +            if (bytesRead == -1) { // success, end of stream already reached +                handleEndOfStream(state, innerState); +                return; +            } + +            state.mGotData = true; +            writeDataToDestination(state, data, bytesRead); +            innerState.mBytesSoFar += bytesRead; +            innerState.mBytesThisSession += bytesRead; +            reportProgress(state, innerState); + +            checkPausedOrCanceled(state); +        } +    } + +    /** +     * Called after a successful completion to take any necessary action on the +     * downloaded file. +     */ +    private void finalizeDestinationFile(State state) throws StopRequest { +        syncDestination(state); +        String tempFilename = state.mFilename; +        String finalFilename = Helpers.generateSaveFileName(mService, mInfo.mFileName); +        if (!state.mFilename.equals(finalFilename)) { +            File startFile = new File(tempFilename); +            File destFile = new File(finalFilename); +            if (mInfo.mTotalBytes != -1 && mInfo.mCurrentBytes == mInfo.mTotalBytes) { +                if (!startFile.renameTo(destFile)) { +                    throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, +                            "unable to finalize destination file"); +                } +            } else { +                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, +                        "file delivered with incorrect size. probably due to network not browser configured"); +            } +        } +    } + +    /** +     * Called just before the thread finishes, regardless of status, to take any +     * necessary action on the downloaded file. +     */ +    private void cleanupDestination(State state, int finalStatus) { +        closeDestination(state); +        if (state.mFilename != null && DownloaderService.isStatusError(finalStatus)) { +            new File(state.mFilename).delete(); +            state.mFilename = null; +        } +    } + +    /** +     * Sync the destination file to storage. +     */ +    private void syncDestination(State state) { +        FileOutputStream downloadedFileStream = null; +        try { +            downloadedFileStream = new FileOutputStream(state.mFilename, true); +            downloadedFileStream.getFD().sync(); +        } catch (FileNotFoundException ex) { +            Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); +        } catch (SyncFailedException ex) { +            Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); +        } catch (IOException ex) { +            Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); +        } catch (RuntimeException ex) { +            Log.w(Constants.TAG, "exception while syncing file: ", ex); +        } finally { +            if (downloadedFileStream != null) { +                try { +                    downloadedFileStream.close(); +                } catch (IOException ex) { +                    Log.w(Constants.TAG, "IOException while closing synced file: ", ex); +                } catch (RuntimeException ex) { +                    Log.w(Constants.TAG, "exception while closing file: ", ex); +                } +            } +        } +    } + +    /** +     * Close the destination output stream. +     */ +    private void closeDestination(State state) { +        try { +            // close the file +            if (state.mStream != null) { +                state.mStream.close(); +                state.mStream = null; +            } +        } catch (IOException ex) { +            if (Constants.LOGV) { +                Log.v(Constants.TAG, "exception when closing the file after download : " + ex); +            } +            // nothing can really be done if the file can't be closed +        } +    } + +    /** +     * Check if the download has been paused or canceled, stopping the request +     * appropriately if it has been. +     */ +    private void checkPausedOrCanceled(State state) throws StopRequest { +        if (mService.getControl() == DownloaderService.CONTROL_PAUSED) { +            int status = mService.getStatus(); +            switch (status) { +                case DownloaderService.STATUS_PAUSED_BY_APP: +                    throw new StopRequest(mService.getStatus(), +                            "download paused"); +            } +        } +    } + +    /** +     * Report download progress through the database if necessary. +     */ +    private void reportProgress(State state, InnerState innerState) { +        long now = System.currentTimeMillis(); +        if (innerState.mBytesSoFar - innerState.mBytesNotified +                > Constants.MIN_PROGRESS_STEP +                && now - innerState.mTimeLastNotification +                > Constants.MIN_PROGRESS_TIME) { +            // we store progress updates to the database here +            mInfo.mCurrentBytes = innerState.mBytesSoFar; +            mDB.updateDownloadCurrentBytes(mInfo); + +            innerState.mBytesNotified = innerState.mBytesSoFar; +            innerState.mTimeLastNotification = now; + +            long totalBytesSoFar = innerState.mBytesThisSession + mService.mBytesSoFar; + +            if (Constants.LOGVV) { +                Log.v(Constants.TAG, "downloaded " + mInfo.mCurrentBytes + " out of " +                        + mInfo.mTotalBytes); +                Log.v(Constants.TAG, "     total " + totalBytesSoFar + " out of " +                        + mService.mTotalLength); +            } + +            mService.notifyUpdateBytes(totalBytesSoFar); +        } +    } + +    /** +     * Write a data buffer to the destination file. +     * +     * @param data buffer containing the data to write +     * @param bytesRead how many bytes to write from the buffer +     */ +    private void writeDataToDestination(State state, byte[] data, int bytesRead) +            throws StopRequest { +        for (;;) { +            try { +                if (state.mStream == null) { +                    state.mStream = new FileOutputStream(state.mFilename, true); +                } +                state.mStream.write(data, 0, bytesRead); +                // we close after every write --- this may be too inefficient +                closeDestination(state); +                return; +            } catch (IOException ex) { +                if (!Helpers.isExternalMediaMounted()) { +                    throw new StopRequest(DownloaderService.STATUS_DEVICE_NOT_FOUND_ERROR, +                            "external media not mounted while writing destination file"); +                } + +                long availableBytes = +                        Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename)); +                if (availableBytes < bytesRead) { +                    throw new StopRequest(DownloaderService.STATUS_INSUFFICIENT_SPACE_ERROR, +                            "insufficient space while writing destination file", ex); +                } +                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, +                        "while writing destination file: " + ex.toString(), ex); +            } +        } +    } + +    /** +     * Called when we've reached the end of the HTTP response stream, to update +     * the database and check for consistency. +     */ +    private void handleEndOfStream(State state, InnerState innerState) throws StopRequest { +        mInfo.mCurrentBytes = innerState.mBytesSoFar; +        // this should always be set from the market +        // if ( innerState.mHeaderContentLength == null ) { +        // mInfo.mTotalBytes = innerState.mBytesSoFar; +        // } +        mDB.updateDownload(mInfo); + +        boolean lengthMismatched = (innerState.mHeaderContentLength != null) +                && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength)); +        if (lengthMismatched) { +            if (cannotResume(innerState)) { +                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, +                        "mismatched content length"); +            } else { +                throw new StopRequest(getFinalStatusForHttpError(state), +                        "closed socket before end of file"); +            } +        } +    } + +    private boolean cannotResume(InnerState innerState) { +        return innerState.mBytesSoFar > 0 && innerState.mHeaderETag == null; +    } + +    /** +     * Read some data from the HTTP response stream, handling I/O errors. +     * +     * @param data buffer to use to read data +     * @param entityStream stream for reading the HTTP response entity +     * @return the number of bytes actually read or -1 if the end of the stream +     *         has been reached +     */ +    private int readFromResponse(State state, InnerState innerState, byte[] data, +            InputStream entityStream) throws StopRequest { +        try { +            return entityStream.read(data); +        } catch (IOException ex) { +            logNetworkState(); +            mInfo.mCurrentBytes = innerState.mBytesSoFar; +            mDB.updateDownload(mInfo); +            if (cannotResume(innerState)) { +                String message = "while reading response: " + ex.toString() +                        + ", can't resume interrupted download with no ETag"; +                throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, +                        message, ex); +            } else { +                throw new StopRequest(getFinalStatusForHttpError(state), +                        "while reading response: " + ex.toString(), ex); +            } +        } +    } + +    /** +     * Open a stream for the HTTP response entity, handling I/O errors. +     * +     * @return an InputStream to read the response entity +     */ +    private InputStream openResponseEntity(State state, HttpURLConnection response) +            throws StopRequest { +        try { +            return response.getInputStream(); +        } catch (IOException ex) { +            logNetworkState(); +            throw new StopRequest(getFinalStatusForHttpError(state), +                    "while getting entity: " + ex.toString(), ex); +        } +    } + +    private void logNetworkState() { +        if (Constants.LOGX) { +            Log.i(Constants.TAG, +                    "Net " +                            + (mService.getNetworkAvailabilityState(mDB) == DownloaderService.NETWORK_OK ? "Up" +                                    : "Down")); +        } +    } + +    /** +     * Read HTTP response headers and take appropriate action, including setting +     * up the destination file and updating the database. +     */ +    private void processResponseHeaders(State state, InnerState innerState, HttpURLConnection response) +            throws StopRequest { +        if (innerState.mContinuingDownload) { +            // ignore response headers on resume requests +            return; +        } + +        readResponseHeaders(state, innerState, response); + +        try { +            state.mFilename = mService.generateSaveFile(mInfo.mFileName, mInfo.mTotalBytes); +        } catch (DownloaderService.GenerateSaveFileError exc) { +            throw new StopRequest(exc.mStatus, exc.mMessage); +        } +        try { +            state.mStream = new FileOutputStream(state.mFilename); +        } catch (FileNotFoundException exc) { +            // make sure the directory exists +            File pathFile = new File(Helpers.getSaveFilePath(mService)); +            try { +                if (pathFile.mkdirs()) { +                    state.mStream = new FileOutputStream(state.mFilename); +                } +            } catch (Exception ex) { +                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, +                        "while opening destination file: " + exc.toString(), exc); +            } +        } +        if (Constants.LOGV) { +            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); +        } + +        updateDatabaseFromHeaders(state, innerState); +        // check connectivity again now that we know the total size +        checkConnectivity(state); +    } + +    /** +     * Update necessary database fields based on values of HTTP response headers +     * that have been read. +     */ +    private void updateDatabaseFromHeaders(State state, InnerState innerState) { +        mInfo.mETag = innerState.mHeaderETag; +        mDB.updateDownload(mInfo); +    } + +    /** +     * Read headers from the HTTP response and store them into local state. +     */ +    private void readResponseHeaders(State state, InnerState innerState, HttpURLConnection response) +            throws StopRequest { +        String value = response.getHeaderField("Content-Disposition"); +        if (value != null) { +            innerState.mHeaderContentDisposition = value; +        } +        value = response.getHeaderField("Content-Location"); +        if (value != null) { +            innerState.mHeaderContentLocation = value; +        } +        value = response.getHeaderField("ETag"); +        if (value != null) { +            innerState.mHeaderETag = value; +        } +        String headerTransferEncoding = null; +        value = response.getHeaderField("Transfer-Encoding"); +        if (value != null) { +            headerTransferEncoding = value; +        } +        String headerContentType = null; +        value = response.getHeaderField("Content-Type"); +        if (value != null) { +            headerContentType = value; +            if (!headerContentType.equals("application/vnd.android.obb")) { +                throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY, +                        "file delivered with incorrect Mime type"); +            } +        } + +        if (headerTransferEncoding == null) { +            long contentLength = response.getContentLength(); +            if (value != null) { +                // this is always set from Market +                if (contentLength != -1 && contentLength != mInfo.mTotalBytes) { +                    // we're most likely on a bad wifi connection -- we should +                    // probably +                    // also look at the mime type --- but the size mismatch is +                    // enough +                    // to tell us that something is wrong here +                    Log.e(Constants.TAG, "Incorrect file size delivered."); +                } else { +                    innerState.mHeaderContentLength = Long.toString(contentLength); +                } +            } +        } else { +            // Ignore content-length with transfer-encoding - 2616 4.4 3 +            if (Constants.LOGVV) { +                Log.v(Constants.TAG, +                        "ignoring content-length because of xfer-encoding"); +            } +        } +        if (Constants.LOGVV) { +            Log.v(Constants.TAG, "Content-Disposition: " + +                    innerState.mHeaderContentDisposition); +            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); +            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); +            Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag); +            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); +        } + +        boolean noSizeInfo = innerState.mHeaderContentLength == null +                && (headerTransferEncoding == null +                || !headerTransferEncoding.equalsIgnoreCase("chunked")); +        if (noSizeInfo) { +            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, +                    "can't know size of download, giving up"); +        } +    } + +    /** +     * Check the HTTP response status and handle anything unusual (e.g. not +     * 200/206). +     */ +    private void handleExceptionalStatus(State state, InnerState innerState, HttpURLConnection connection, int responseCode) +            throws StopRequest, RetryDownload { +        if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { +            handleServiceUnavailable(state, connection); +        } +        int expectedStatus = innerState.mContinuingDownload ? 206 +                : DownloaderService.STATUS_SUCCESS; +        if (responseCode != expectedStatus) { +            handleOtherStatus(state, innerState, responseCode); +        } else { +            // no longer redirected +            state.mRedirectCount = 0; +        } +    } + +    /** +     * Handle a status that we don't know how to deal with properly. +     */ +    private void handleOtherStatus(State state, InnerState innerState, int statusCode) +            throws StopRequest { +        int finalStatus; +        if (DownloaderService.isStatusError(statusCode)) { +            finalStatus = statusCode; +        } else if (statusCode >= 300 && statusCode < 400) { +            finalStatus = DownloaderService.STATUS_UNHANDLED_REDIRECT; +        } else if (innerState.mContinuingDownload && statusCode == DownloaderService.STATUS_SUCCESS) { +            finalStatus = DownloaderService.STATUS_CANNOT_RESUME; +        } else { +            finalStatus = DownloaderService.STATUS_UNHANDLED_HTTP_CODE; +        } +        throw new StopRequest(finalStatus, "http error " + statusCode); +    } + +    /** +     * Add headers for this download to the HTTP request to allow for resume. +     */ +    private void addRequestHeaders(InnerState innerState, HttpURLConnection request) { +        if (innerState.mContinuingDownload) { +            if (innerState.mHeaderETag != null) { +                request.setRequestProperty("If-Match", innerState.mHeaderETag); +            } +            request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-"); +        } +    } + +    /** +     * Handle a 503 Service Unavailable status by processing the Retry-After +     * header. +     */ +    private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest { +        if (Constants.LOGVV) { +            Log.v(Constants.TAG, "got HTTP response code 503"); +        } +        state.mCountRetry = true; +        String retryAfterValue = connection.getHeaderField("Retry-After"); +        if (retryAfterValue != null) { +            try { +                if (Constants.LOGVV) { +                    Log.v(Constants.TAG, "Retry-After :" + retryAfterValue); +                } +                state.mRetryAfter = Integer.parseInt(retryAfterValue); +                if (state.mRetryAfter < 0) { +                    state.mRetryAfter = 0; +                } else { +                    if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { +                        state.mRetryAfter = Constants.MIN_RETRY_AFTER; +                    } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { +                        state.mRetryAfter = Constants.MAX_RETRY_AFTER; +                    } +                    state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); +                    state.mRetryAfter *= 1000; +                } +            } catch (NumberFormatException ex) { +                // ignored - retryAfter stays 0 in this case. +            } +        } +        throw new StopRequest(DownloaderService.STATUS_WAITING_TO_RETRY, +                "got 503 Service Unavailable, will retry later"); +    } + +    /** +     * Send the request to the server, handling any I/O exceptions. +     */ +    private int sendRequest(State state, HttpURLConnection request) +            throws StopRequest { +        try { +            return request.getResponseCode(); +        } catch (IllegalArgumentException ex) { +            throw new StopRequest(DownloaderService.STATUS_HTTP_DATA_ERROR, +                    "while trying to execute request: " + ex.toString(), ex); +        } catch (IOException ex) { +            logNetworkState(); +            throw new StopRequest(getFinalStatusForHttpError(state), +                    "while trying to execute request: " + ex.toString(), ex); +        } +    } + +    private int getFinalStatusForHttpError(State state) { +        if (mService.getNetworkAvailabilityState(mDB) != DownloaderService.NETWORK_OK) { +            return DownloaderService.STATUS_WAITING_FOR_NETWORK; +        } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { +            state.mCountRetry = true; +            return DownloaderService.STATUS_WAITING_TO_RETRY; +        } else { +            Log.w(Constants.TAG, "reached max retries for " + mInfo.mNumFailed); +            return DownloaderService.STATUS_HTTP_DATA_ERROR; +        } +    } + +    /** +     * Prepare the destination file to receive data. If the file already exists, +     * we'll set up appropriately for resumption. +     */ +    private void setupDestinationFile(State state, InnerState innerState) +            throws StopRequest { +        if (state.mFilename != null) { // only true if we've already run a +                                       // thread for this download +            if (!Helpers.isFilenameValid(state.mFilename)) { +                // this should never happen +                throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, +                        "found invalid internal destination filename"); +            } +            // We're resuming a download that got interrupted +            File f = new File(state.mFilename); +            if (f.exists()) { +                long fileLength = f.length(); +                if (fileLength == 0) { +                    // The download hadn't actually started, we can restart from +                    // scratch +                    f.delete(); +                    state.mFilename = null; +                } else if (mInfo.mETag == null) { +                    // This should've been caught upon failure +                    f.delete(); +                    throw new StopRequest(DownloaderService.STATUS_CANNOT_RESUME, +                            "Trying to resume a download that can't be resumed"); +                } else { +                    // All right, we'll be able to resume this download +                    try { +                        state.mStream = new FileOutputStream(state.mFilename, true); +                    } catch (FileNotFoundException exc) { +                        throw new StopRequest(DownloaderService.STATUS_FILE_ERROR, +                                "while opening destination for resuming: " + exc.toString(), exc); +                    } +                    innerState.mBytesSoFar = (int) fileLength; +                    if (mInfo.mTotalBytes != -1) { +                        innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); +                    } +                    innerState.mHeaderETag = mInfo.mETag; +                    innerState.mContinuingDownload = true; +                } +            } +        } + +        if (state.mStream != null) { +            closeDestination(state); +        } +    } + +    /** +     * Stores information about the completed download, and notifies the +     * initiating application. +     */ +    private void notifyDownloadCompleted( +            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, +            String filename) { +        updateDownloadDatabase( +                status, countRetry, retryAfter, redirectCount, gotData, filename); +        if (DownloaderService.isStatusCompleted(status)) { +            // TBD: send status update? +        } +    } + +    private void updateDownloadDatabase( +            int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData, +            String filename) { +        mInfo.mStatus = status; +        mInfo.mRetryAfter = retryAfter; +        mInfo.mRedirectCount = redirectCount; +        mInfo.mLastMod = System.currentTimeMillis(); +        if (!countRetry) { +            mInfo.mNumFailed = 0; +        } else if (gotData) { +            mInfo.mNumFailed = 1; +        } else { +            mInfo.mNumFailed++; +        } +        mDB.updateDownload(mInfo); +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java new file mode 100644 index 0000000000..8d41a76900 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloaderService.java @@ -0,0 +1,1346 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import com.google.android.vending.expansion.downloader.Constants; +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; +import com.google.android.vending.expansion.downloader.IDownloaderService; +import com.google.android.vending.expansion.downloader.IStub; +import com.google.android.vending.licensing.AESObfuscator; +import com.google.android.vending.licensing.APKExpansionPolicy; +import com.google.android.vending.licensing.LicenseChecker; +import com.google.android.vending.licensing.LicenseCheckerCallback; +import com.google.android.vending.licensing.Policy; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Messenger; +import android.os.SystemClock; +import android.provider.Settings.Secure; +import android.telephony.TelephonyManager; +import android.util.Log; + +// -- GODOT start -- +import android.annotation.SuppressLint; +// -- GODOT end -- + +import java.io.File; + +/** + * Performs the background downloads requested by applications that use the + * Downloads provider. This service does not run as a foreground task, so + * Android may kill it off at will, but it will try to restart itself if it can. + * Note that Android by default will kill off any process that has an open file + * handle on the shared (SD Card) partition if the partition is unmounted. + */ +public abstract class DownloaderService extends CustomIntentService implements IDownloaderService { + +    public DownloaderService() { +        super("LVLDownloadService"); +    } + +    private static final String LOG_TAG = "LVLDL"; + +    // the following NETWORK_* constants are used to indicates specific reasons +    // for disallowing a +    // download from using a network, since specific causes can require special +    // handling + +    /** +     * The network is usable for the given download. +     */ +    public static final int NETWORK_OK = 1; + +    /** +     * There is no network connectivity. +     */ +    public static final int NETWORK_NO_CONNECTION = 2; + +    /** +     * The download exceeds the maximum size for this network. +     */ +    public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; + +    /** +     * The download exceeds the recommended maximum size for this network, the +     * user must confirm for this download to proceed without WiFi. +     */ +    public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; + +    /** +     * The current connection is roaming, and the download can't proceed over a +     * roaming connection. +     */ +    public static final int NETWORK_CANNOT_USE_ROAMING = 5; + +    /** +     * The app requesting the download specific that it can't use the current +     * network connection. +     */ +    public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; + +    /** +     * For intents used to notify the user that a download exceeds a size +     * threshold, if this extra is true, WiFi is required for this download +     * size; otherwise, it is only recommended. +     */ +    public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; +    public static final String EXTRA_FILE_NAME = "downloadId"; + +    /** +     * Used with DOWNLOAD_STATUS +     */ +    public static final String EXTRA_STATUS_STATE = "ESS"; +    public static final String EXTRA_STATUS_TOTAL_SIZE = "ETS"; +    public static final String EXTRA_STATUS_CURRENT_FILE_SIZE = "CFS"; +    public static final String EXTRA_STATUS_TOTAL_PROGRESS = "TFP"; +    public static final String EXTRA_STATUS_CURRENT_PROGRESS = "CFP"; + +    public static final String ACTION_DOWNLOADS_CHANGED = "downloadsChanged"; + +    /** +     * Broadcast intent action sent by the download manager when a download +     * completes. +     */ +    public final static String ACTION_DOWNLOAD_COMPLETE = "lvldownloader.intent.action.DOWNLOAD_COMPLETE"; + +    /** +     * Broadcast intent action sent by the download manager when download status +     * changes. +     */ +    public final static String ACTION_DOWNLOAD_STATUS = "lvldownloader.intent.action.DOWNLOAD_STATUS"; + +    /* +     * Lists the states that the download manager can set on a download to +     * notify applications of the download progress. The codes follow the HTTP +     * families:<br> 1xx: informational<br> 2xx: success<br> 3xx: redirects (not +     * used by the download manager)<br> 4xx: client errors<br> 5xx: server +     * errors +     */ + +    /** +     * Returns whether the status is informational (i.e. 1xx). +     */ +    public static boolean isStatusInformational(int status) { +        return (status >= 100 && status < 200); +    } + +    /** +     * Returns whether the status is a success (i.e. 2xx). +     */ +    public static boolean isStatusSuccess(int status) { +        return (status >= 200 && status < 300); +    } + +    /** +     * Returns whether the status is an error (i.e. 4xx or 5xx). +     */ +    public static boolean isStatusError(int status) { +        return (status >= 400 && status < 600); +    } + +    /** +     * Returns whether the status is a client error (i.e. 4xx). +     */ +    public static boolean isStatusClientError(int status) { +        return (status >= 400 && status < 500); +    } + +    /** +     * Returns whether the status is a server error (i.e. 5xx). +     */ +    public static boolean isStatusServerError(int status) { +        return (status >= 500 && status < 600); +    } + +    /** +     * Returns whether the download has completed (either with success or +     * error). +     */ +    public static boolean isStatusCompleted(int status) { +        return (status >= 200 && status < 300) +                || (status >= 400 && status < 600); +    } + +    /** +     * This download hasn't stated yet +     */ +    public static final int STATUS_PENDING = 190; + +    /** +     * This download has started +     */ +    public static final int STATUS_RUNNING = 192; + +    /** +     * This download has been paused by the owning app. +     */ +    public static final int STATUS_PAUSED_BY_APP = 193; + +    /** +     * This download encountered some network error and is waiting before +     * retrying the request. +     */ +    public static final int STATUS_WAITING_TO_RETRY = 194; + +    /** +     * This download is waiting for network connectivity to proceed. +     */ +    public static final int STATUS_WAITING_FOR_NETWORK = 195; + +    /** +     * This download is waiting for a Wi-Fi connection to proceed or for +     * permission to download over cellular. +     */ +    public static final int STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION = 196; + +    /** +     * This download is waiting for a Wi-Fi connection to proceed. +     */ +    public static final int STATUS_QUEUED_FOR_WIFI = 197; + +    /** +     * This download has successfully completed. Warning: there might be other +     * status values that indicate success in the future. Use isSucccess() to +     * capture the entire category. +     * +     * @hide +     */ +    public static final int STATUS_SUCCESS = 200; + +    /** +     * The requested URL is no longer available +     */ +    public static final int STATUS_FORBIDDEN = 403; + +    /** +     * The file was delivered incorrectly +     */ +    public static final int STATUS_FILE_DELIVERED_INCORRECTLY = 487; + +    /** +     * The requested destination file already exists. +     */ +    public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; + +    /** +     * Some possibly transient error occurred, but we can't resume the download. +     */ +    public static final int STATUS_CANNOT_RESUME = 489; + +    /** +     * This download was canceled +     * +     * @hide +     */ +    public static final int STATUS_CANCELED = 490; + +    /** +     * This download has completed with an error. Warning: there will be other +     * status values that indicate errors in the future. Use isStatusError() to +     * capture the entire category. +     */ +    public static final int STATUS_UNKNOWN_ERROR = 491; + +    /** +     * This download couldn't be completed because of a storage issue. +     * Typically, that's because the filesystem is missing or full. Use the more +     * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and +     * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. +     * +     * @hide +     */ +    public static final int STATUS_FILE_ERROR = 492; + +    /** +     * This download couldn't be completed because of an HTTP redirect response +     * that the download manager couldn't handle. +     * +     * @hide +     */ +    public static final int STATUS_UNHANDLED_REDIRECT = 493; + +    /** +     * This download couldn't be completed because of an unspecified unhandled +     * HTTP code. +     * +     * @hide +     */ +    public static final int STATUS_UNHANDLED_HTTP_CODE = 494; + +    /** +     * This download couldn't be completed because of an error receiving or +     * processing data at the HTTP level. +     * +     * @hide +     */ +    public static final int STATUS_HTTP_DATA_ERROR = 495; + +    /** +     * This download couldn't be completed because of an HttpException while +     * setting up the request. +     * +     * @hide +     */ +    public static final int STATUS_HTTP_EXCEPTION = 496; + +    /** +     * This download couldn't be completed because there were too many +     * redirects. +     * +     * @hide +     */ +    public static final int STATUS_TOO_MANY_REDIRECTS = 497; + +    /** +     * This download couldn't be completed due to insufficient storage space. +     * Typically, this is because the SD card is full. +     * +     * @hide +     */ +    public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + +    /** +     * This download couldn't be completed because no external storage device +     * was found. Typically, this is because the SD card is not mounted. +     * +     * @hide +     */ +    public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + +    /** +     * This download is allowed to run. +     * +     * @hide +     */ +    public static final int CONTROL_RUN = 0; + +    /** +     * This download must pause at the first opportunity. +     * +     * @hide +     */ +    public static final int CONTROL_PAUSED = 1; + +    /** +     * This download is visible but only shows in the notifications while it's +     * in progress. +     * +     * @hide +     */ +    public static final int VISIBILITY_VISIBLE = 0; + +    /** +     * This download is visible and shows in the notifications while in progress +     * and after completion. +     * +     * @hide +     */ +    public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; + +    /** +     * This download doesn't show in the UI or in the notifications. +     * +     * @hide +     */ +    public static final int VISIBILITY_HIDDEN = 2; + +    /** +     * Bit flag for setAllowedNetworkTypes corresponding to +     * {@link ConnectivityManager#TYPE_MOBILE}. +     */ +    public static final int NETWORK_MOBILE = 1 << 0; + +    /** +     * Bit flag for setAllowedNetworkTypes corresponding to +     * {@link ConnectivityManager#TYPE_WIFI}. +     */ +    public static final int NETWORK_WIFI = 1 << 1; + +    private final static String TEMP_EXT = ".tmp"; + +    /** +     * Service thread status +     */ +    private static boolean sIsRunning; + +    @Override +    public IBinder onBind(Intent paramIntent) { +        Log.d(Constants.TAG, "Service Bound"); +        return this.mServiceMessenger.getBinder(); +    } + +    /** +     * Network state. +     */ +    private boolean mIsConnected; +    private boolean mIsFailover; +    private boolean mIsCellularConnection; +    private boolean mIsRoaming; +    private boolean mIsAtLeast3G; +    private boolean mIsAtLeast4G; +    private boolean mStateChanged; + +    /** +     * Download state +     */ +    private int mControl; +    private int mStatus; + +    public boolean isWiFi() { +        return mIsConnected && !mIsCellularConnection; +    } + +    /** +     * Bindings to important services +     */ +    private ConnectivityManager mConnectivityManager; +    private WifiManager mWifiManager; + +    /** +     * Package we are downloading for (defaults to package of application) +     */ +    private PackageInfo mPackageInfo; + +    /** +     * Byte counts +     */ +    long mBytesSoFar; +    long mTotalLength; +    int mFileCount; + +    /** +     * Used for calculating time remaining and speed +     */ +    long mBytesAtSample; +    long mMillisecondsAtSample; +    float mAverageDownloadSpeed; + +    /** +     * Our binding to the network state broadcasts +     */ +    private BroadcastReceiver mConnReceiver; +    final private IStub mServiceStub = DownloaderServiceMarshaller.CreateStub(this); +    final private Messenger mServiceMessenger = mServiceStub.getMessenger(); +    private Messenger mClientMessenger; +    private DownloadNotification mNotification; +    private PendingIntent mPendingIntent; +    private PendingIntent mAlarmIntent; + +    /** +     * Updates the network type based upon the type and subtype returned from +     * the connectivity manager. Subtype is only used for cellular signals. +     * +     * @param type +     * @param subType +     */ +    private void updateNetworkType(int type, int subType) { +        switch (type) { +            case ConnectivityManager.TYPE_WIFI: +            case ConnectivityManager.TYPE_ETHERNET: +            case ConnectivityManager.TYPE_BLUETOOTH: +                mIsCellularConnection = false; +                mIsAtLeast3G = false; +                mIsAtLeast4G = false; +                break; +            case ConnectivityManager.TYPE_WIMAX: +                mIsCellularConnection = true; +                mIsAtLeast3G = true; +                mIsAtLeast4G = true; +                break; +            case ConnectivityManager.TYPE_MOBILE: +                mIsCellularConnection = true; +                switch (subType) { +                    case TelephonyManager.NETWORK_TYPE_1xRTT: +                    case TelephonyManager.NETWORK_TYPE_CDMA: +                    case TelephonyManager.NETWORK_TYPE_EDGE: +                    case TelephonyManager.NETWORK_TYPE_GPRS: +                    case TelephonyManager.NETWORK_TYPE_IDEN: +                        mIsAtLeast3G = false; +                        mIsAtLeast4G = false; +                        break; +                    case TelephonyManager.NETWORK_TYPE_HSDPA: +                    case TelephonyManager.NETWORK_TYPE_HSUPA: +                    case TelephonyManager.NETWORK_TYPE_HSPA: +                    case TelephonyManager.NETWORK_TYPE_EVDO_0: +                    case TelephonyManager.NETWORK_TYPE_EVDO_A: +                    case TelephonyManager.NETWORK_TYPE_UMTS: +                        mIsAtLeast3G = true; +                        mIsAtLeast4G = false; +                        break; +                    case TelephonyManager.NETWORK_TYPE_LTE: // 4G +                    case TelephonyManager.NETWORK_TYPE_EHRPD: // 3G ++ interop +                                                              // with 4G +                    case TelephonyManager.NETWORK_TYPE_HSPAP: // 3G ++ but +                                                              // marketed as +                                                              // 4G +                        mIsAtLeast3G = true; +                        mIsAtLeast4G = true; +                        break; +                    default: +                        mIsCellularConnection = false; +                        mIsAtLeast3G = false; +                        mIsAtLeast4G = false; +                } +        } +    } + +    private void updateNetworkState(NetworkInfo info) { +        boolean isConnected = mIsConnected; +        boolean isFailover = mIsFailover; +        boolean isCellularConnection = mIsCellularConnection; +        boolean isRoaming = mIsRoaming; +        boolean isAtLeast3G = mIsAtLeast3G; +        if (null != info) { +            mIsRoaming = info.isRoaming(); +            mIsFailover = info.isFailover(); +            mIsConnected = info.isConnected(); +            updateNetworkType(info.getType(), info.getSubtype()); +        } else { +            mIsRoaming = false; +            mIsFailover = false; +            mIsConnected = false; +            updateNetworkType(-1, -1); +        } +        mStateChanged = (mStateChanged || isConnected != mIsConnected +                || isFailover != mIsFailover +                || isCellularConnection != mIsCellularConnection +                || isRoaming != mIsRoaming || isAtLeast3G != mIsAtLeast3G); +        if (Constants.LOGVV) { +            if (mStateChanged) { +                Log.v(LOG_TAG, "Network state changed: "); +                Log.v(LOG_TAG, "Starting State: " + +                        (isConnected ? "Connected " : "Not Connected ") + +                        (isCellularConnection ? "Cellular " : "WiFi ") + +                        (isRoaming ? "Roaming " : "Local ") + +                        (isAtLeast3G ? "3G+ " : "<3G ")); +                Log.v(LOG_TAG, "Ending State: " + +                        (mIsConnected ? "Connected " : "Not Connected ") + +                        (mIsCellularConnection ? "Cellular " : "WiFi ") + +                        (mIsRoaming ? "Roaming " : "Local ") + +                        (mIsAtLeast3G ? "3G+ " : "<3G ")); + +                if (isServiceRunning()) { +                    if (mIsRoaming) { +                        mStatus = STATUS_WAITING_FOR_NETWORK; +                        mControl = CONTROL_PAUSED; +                    } else if (mIsCellularConnection) { +                        DownloadsDB db = DownloadsDB.getDB(this); +                        int flags = db.getFlags(); +                        if (0 == (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { +                            mStatus = STATUS_QUEUED_FOR_WIFI; +                            mControl = CONTROL_PAUSED; +                        } +                    } +                } + +            } +        } +    } + +    /** +     * Polls the network state, setting the flags appropriately. +     */ +    void pollNetworkState() { +        if (null == mConnectivityManager) { +            mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); +        } +        if (null == mWifiManager) { +            mWifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); +        } +        if (mConnectivityManager == null) { +            Log.w(Constants.TAG, +                    "couldn't get connectivity manager to poll network state"); +        } else { +            @SuppressLint("MissingPermission") +            NetworkInfo activeInfo = mConnectivityManager +                    .getActiveNetworkInfo(); +            updateNetworkState(activeInfo); +        } +    } + +    public static final int NO_DOWNLOAD_REQUIRED = 0; +    public static final int LVL_CHECK_REQUIRED = 1; +    public static final int DOWNLOAD_REQUIRED = 2; + +    public static final String EXTRA_PACKAGE_NAME = "EPN"; +    public static final String EXTRA_PENDING_INTENT = "EPI"; +    public static final String EXTRA_MESSAGE_HANDLER = "EMH"; + +    /** +     * Returns true if the LVL check is required +     * +     * @param db a downloads DB synchronized with the latest state +     * @param pi the package info for the project +     * @return returns true if the filenames need to be returned +     */ +    private static boolean isLVLCheckRequired(DownloadsDB db, PackageInfo pi) { +        // we need to update the LVL check and get a successful status to +        // proceed +        if (db.mVersionCode != pi.versionCode) { +            return true; +        } +        return false; +    } + +    /** +     * Careful! Only use this internally. +     * +     * @return whether we think the service is running +     */ +    private static synchronized boolean isServiceRunning() { +        return sIsRunning; +    } + +    private static synchronized void setServiceRunning(boolean isRunning) { +        sIsRunning = isRunning; +    } + +    public static int startDownloadServiceIfRequired(Context context, +            Intent intent, Class<?> serviceClass) throws NameNotFoundException { +        final PendingIntent pendingIntent = (PendingIntent) intent +                .getParcelableExtra(EXTRA_PENDING_INTENT); +        return startDownloadServiceIfRequired(context, pendingIntent, +                serviceClass); +    } + +    public static int startDownloadServiceIfRequired(Context context, +            PendingIntent pendingIntent, Class<?> serviceClass) +            throws NameNotFoundException +    { +        String packageName = context.getPackageName(); +        String className = serviceClass.getName(); + +        return startDownloadServiceIfRequired(context, pendingIntent, +                packageName, className); +    } + +    /** +     * Starts the download if necessary. This function starts a flow that does ` +     * many things. 1) Checks to see if the APK version has been checked and the +     * metadata database updated 2) If the APK version does not match, checks +     * the new LVL status to see if a new download is required 3) If the APK +     * version does match, then checks to see if the download(s) have been +     * completed 4) If the downloads have been completed, returns +     * NO_DOWNLOAD_REQUIRED The idea is that this can be called during the +     * startup of an application to quickly ascertain if the application needs +     * to wait to hear about any updated APK expansion files. Note that this +     * does mean that the application MUST be run for the first time with a +     * network connection, even if Market delivers all of the files. +     * +     * @param context +     * @param pendingIntent +     * @return true if the app should wait for more guidance from the +     *         downloader, false if the app can continue +     * @throws NameNotFoundException +     */ +    public static int startDownloadServiceIfRequired(Context context, +            PendingIntent pendingIntent, String classPackage, String className) +            throws NameNotFoundException { +        // first: do we need to do an LVL update? +        // we begin by getting our APK version from the package manager +        final PackageInfo pi = context.getPackageManager().getPackageInfo( +                context.getPackageName(), 0); + +        int status = NO_DOWNLOAD_REQUIRED; + +        // the database automatically reads the metadata for version code +        // and download status when the instance is created +        DownloadsDB db = DownloadsDB.getDB(context); + +        // we need to update the LVL check and get a successful status to +        // proceed +        if (isLVLCheckRequired(db, pi)) { +            status = LVL_CHECK_REQUIRED; +        } +        // we don't have to update LVL. do we still have a download to start? +        if (db.mStatus == 0) { +            DownloadInfo[] infos = db.getDownloads(); +            if (null != infos) { +                for (DownloadInfo info : infos) { +                    if (!Helpers.doesFileExist(context, info.mFileName, info.mTotalBytes, true)) { +                        status = DOWNLOAD_REQUIRED; +                        db.updateStatus(-1); +                        break; +                    } +                } +            } +        } else { +            status = DOWNLOAD_REQUIRED; +        } +        switch (status) { +            case DOWNLOAD_REQUIRED: +            case LVL_CHECK_REQUIRED: +                Intent fileIntent = new Intent(); +                fileIntent.setClassName(classPackage, className); +                fileIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); +                context.startService(fileIntent); +                break; +        } +        return status; +    } + +    @Override +    public void requestAbortDownload() { +        mControl = CONTROL_PAUSED; +        mStatus = STATUS_CANCELED; +    } + +    @Override +    public void requestPauseDownload() { +        mControl = CONTROL_PAUSED; +        mStatus = STATUS_PAUSED_BY_APP; +    } + +    @Override +    public void setDownloadFlags(int flags) { +        DownloadsDB.getDB(this).updateFlags(flags); +    } + +    @Override +    public void requestContinueDownload() { +        if (mControl == CONTROL_PAUSED) { +            mControl = CONTROL_RUN; +        } +        Intent fileIntent = new Intent(this, this.getClass()); +        fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); +        this.startService(fileIntent); +    } + +    public abstract String getPublicKey(); + +    public abstract byte[] getSALT(); + +    public abstract String getAlarmReceiverClassName(); + +    private class LVLRunnable implements Runnable { +        LVLRunnable(Context context, PendingIntent intent) { +            mContext = context; +            mPendingIntent = intent; +        } + +        final Context mContext; + +        @Override +        public void run() { +            setServiceRunning(true); +            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_FETCHING_URL); +            String deviceId = Secure.getString(mContext.getContentResolver(), +                    Secure.ANDROID_ID); + +            final APKExpansionPolicy aep = new APKExpansionPolicy(mContext, +                    new AESObfuscator(getSALT(), mContext.getPackageName(), deviceId)); + +            // reset our policy back to the start of the world to force a +            // re-check +            aep.resetPolicy(); + +            // let's try and get the OBB file from LVL first +            // Construct the LicenseChecker with a Policy. +            final LicenseChecker checker = new LicenseChecker(mContext, aep, +                    getPublicKey() // Your public licensing key. +            ); +            checker.checkAccess(new LicenseCheckerCallback() { + +                @Override +                public void allow(int reason) { +                    try { +                        int count = aep.getExpansionURLCount(); +                        DownloadsDB db = DownloadsDB.getDB(mContext); +                        int status = 0; +                        if (count != 0) { +                            for (int i = 0; i < count; i++) { +                                String currentFileName = aep +                                        .getExpansionFileName(i); +                                if (null != currentFileName) { +                                    DownloadInfo di = new DownloadInfo(i, +                                            currentFileName, mContext.getPackageName()); + +                                    long fileSize = aep.getExpansionFileSize(i); +                                    if (handleFileUpdated(db, i, currentFileName, +                                            fileSize)) { +                                        status |= -1; +                                        di.resetDownload(); +                                        di.mUri = aep.getExpansionURL(i); +                                        di.mTotalBytes = fileSize; +                                        di.mStatus = status; +                                        db.updateDownload(di); +                                    } else { +                                        // we need to read the download +                                        // information +                                        // from +                                        // the database +                                        DownloadInfo dbdi = db +                                                .getDownloadInfoByFileName(di.mFileName); +                                        if (null == dbdi) { +                                            // the file exists already and is +                                            // the +                                            // correct size +                                            // was delivered by Market or +                                            // through +                                            // another mechanism +                                            Log.d(LOG_TAG, "file " + di.mFileName +                                                    + " found. Not downloading."); +                                            di.mStatus = STATUS_SUCCESS; +                                            di.mTotalBytes = fileSize; +                                            di.mCurrentBytes = fileSize; +                                            di.mUri = aep.getExpansionURL(i); +                                            db.updateDownload(di); +                                        } else if (dbdi.mStatus != STATUS_SUCCESS) { +                                            // we just update the URL +                                            dbdi.mUri = aep.getExpansionURL(i); +                                            db.updateDownload(dbdi); +                                            status |= -1; +                                        } +                                    } +                                } +                            } +                        } +                        // first: do we need to do an LVL update? +                        // we begin by getting our APK version from the package +                        // manager +                        PackageInfo pi; +                        try { +                            pi = mContext.getPackageManager().getPackageInfo( +                                    mContext.getPackageName(), 0); +                            db.updateMetadata(pi.versionCode, status); +                            Class<?> serviceClass = DownloaderService.this.getClass(); +                            switch (startDownloadServiceIfRequired(mContext, mPendingIntent, +                                    serviceClass)) { +                                case NO_DOWNLOAD_REQUIRED: +                                    mNotification +                                            .onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); +                                    break; +                                case LVL_CHECK_REQUIRED: +                                    // DANGER WILL ROBINSON! +                                    Log.e(LOG_TAG, "In LVL checking loop!"); +                                    mNotification +                                            .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); +                                    throw new RuntimeException( +                                            "Error with LVL checking and database integrity"); +                                case DOWNLOAD_REQUIRED: +                                    // do nothing. the download will notify the +                                    // application +                                    // when things are done +                                    break; +                            } +                        } catch (NameNotFoundException e1) { +                            e1.printStackTrace(); +                            throw new RuntimeException( +                                    "Error with getting information from package name"); +                        } +                    } finally { +                        setServiceRunning(false); +                    } +                } + +                @Override +                public void dontAllow(int reason) { +                    try +                    { +                        switch (reason) { +                            case Policy.NOT_LICENSED: +                                mNotification +                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_UNLICENSED); +                                break; +                            case Policy.RETRY: +                                mNotification +                                        .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); +                                break; +                        } +                    } finally { +                        setServiceRunning(false); +                    } + +                } + +                @Override +                public void applicationError(int errorCode) { +                    try { +                        mNotification +                                .onDownloadStateChanged(IDownloaderClient.STATE_FAILED_FETCHING_URL); +                    } finally { +                        setServiceRunning(false); +                    } +                } + +            }); + +        } + +    }; + +    /** +     * Updates the LVL information from the server. +     * +     * @param context +     */ +    public void updateLVL(final Context context) { +        Context c = context.getApplicationContext(); +        Handler h = new Handler(c.getMainLooper()); +        h.post(new LVLRunnable(c, mPendingIntent)); +    } + +    /** +     * The APK has been updated and a filename has been sent down from the +     * Market call. If the file has the same name as the previous file, we do +     * nothing as the file is guaranteed to be the same. If the file does not +     * have the same name, we download it if it hasn't already been delivered by +     * Market. +     * +     * @param index the index of the file from market (0 = main, 1 = patch) +     * @param filename the name of the new file +     * @param fileSize the size of the new file +     * @return +     */ +    public boolean handleFileUpdated(DownloadsDB db, int index, +            String filename, long fileSize) { +        DownloadInfo di = db.getDownloadInfoByFileName(filename); +        if (null != di) { +            String oldFile = di.mFileName; +            // cleanup +            if (null != oldFile) { +                if (filename.equals(oldFile)) { +                    return false; +                } + +                // remove partially downloaded file if it is there +                String deleteFile = Helpers.generateSaveFileName(this, oldFile); +                File f = new File(deleteFile); +                if (f.exists()) +                    f.delete(); +            } +        } +        return !Helpers.doesFileExist(this, filename, fileSize, true); +    } + +    private void scheduleAlarm(long wakeUp) { +        AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); +        if (alarms == null) { +            Log.e(Constants.TAG, "couldn't get alarm manager"); +            return; +        } + +        if (Constants.LOGV) { +            Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); +        } + +        String className = getAlarmReceiverClassName(); +        Intent intent = new Intent(Constants.ACTION_RETRY); +        intent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); +        intent.setClassName(this.getPackageName(), +                className); +        mAlarmIntent = PendingIntent.getBroadcast(this, 0, intent, +                PendingIntent.FLAG_ONE_SHOT); +        alarms.set( +                AlarmManager.RTC_WAKEUP, +                System.currentTimeMillis() + wakeUp, mAlarmIntent +                ); +    } + +    private void cancelAlarms() { +        if (null != mAlarmIntent) { +            AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); +            if (alarms == null) { +                Log.e(Constants.TAG, "couldn't get alarm manager"); +                return; +            } +            alarms.cancel(mAlarmIntent); +            mAlarmIntent = null; +        } +    } + +    /** +     * We use this to track network state, such as when WiFi, Cellular, etc. is +     * enabled when downloads are paused or in progress. +     */ +    private class InnerBroadcastReceiver extends BroadcastReceiver { +        final Service mService; + +        InnerBroadcastReceiver(Service service) { +            mService = service; +        } + +        @Override +        public void onReceive(Context context, Intent intent) { +            pollNetworkState(); +            if (mStateChanged +                    && !isServiceRunning()) { +                Log.d(Constants.TAG, "InnerBroadcastReceiver Called"); +                Intent fileIntent = new Intent(context, mService.getClass()); +                fileIntent.putExtra(EXTRA_PENDING_INTENT, mPendingIntent); +                // send a new intent to the service +                context.startService(fileIntent); +            } +        } +    }; + +    /** +     * This is the main thread for the Downloader. This thread is responsible +     * for queuing up downloads and other goodness. +     */ +    @Override +    protected void onHandleIntent(Intent intent) { +        setServiceRunning(true); +        try { +            // the database automatically reads the metadata for version code +            // and download status when the instance is created +            DownloadsDB db = DownloadsDB.getDB(this); +            final PendingIntent pendingIntent = (PendingIntent) intent +                    .getParcelableExtra(EXTRA_PENDING_INTENT); + +            if (null != pendingIntent) +            { +                mNotification.setClientIntent(pendingIntent); +                mPendingIntent = pendingIntent; +            } else if (null != mPendingIntent) { +                mNotification.setClientIntent(mPendingIntent); +            } else { +                Log.e(LOG_TAG, "Downloader started in bad state without notification intent."); +                return; +            } + +            // when the LVL check completes, a successful response will update +            // the service +            if (isLVLCheckRequired(db, mPackageInfo)) { +                updateLVL(this); +                return; +            } + +            // get each download +            DownloadInfo[] infos = db.getDownloads(); +            mBytesSoFar = 0; +            mTotalLength = 0; +            mFileCount = infos.length; +            for (DownloadInfo info : infos) { +                // We do an (simple) integrity check on each file, just to make +                // sure +                if (info.mStatus == STATUS_SUCCESS) { +                    // verify that the file matches the state +                    if (!Helpers.doesFileExist(this, info.mFileName, info.mTotalBytes, true)) { +                        info.mStatus = 0; +                        info.mCurrentBytes = 0; +                    } +                } +                // get aggregate data +                mTotalLength += info.mTotalBytes; +                mBytesSoFar += info.mCurrentBytes; +            } + +            // loop through all downloads and fetch them +            pollNetworkState(); +            if (null == mConnReceiver) { + +                /** +                 * We use this to track network state, such as when WiFi, +                 * Cellular, etc. is enabled when downloads are paused or in +                 * progress. +                 */ +                mConnReceiver = new InnerBroadcastReceiver(this); +                IntentFilter intentFilter = new IntentFilter( +                        ConnectivityManager.CONNECTIVITY_ACTION); +                intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); +                registerReceiver(mConnReceiver, intentFilter); +            } + +            for (DownloadInfo info : infos) { +                long startingCount = info.mCurrentBytes; + +                if (info.mStatus != STATUS_SUCCESS) { +                    DownloadThread dt = new DownloadThread(info, this, mNotification); +                    cancelAlarms(); +                    scheduleAlarm(Constants.ACTIVE_THREAD_WATCHDOG); +                    dt.run(); +                    cancelAlarms(); +                } +                db.updateFromDb(info); +                boolean setWakeWatchdog = false; +                int notifyStatus; +                switch (info.mStatus) { +                    case STATUS_FORBIDDEN: +                        // the URL is out of date +                        updateLVL(this); +                        return; +                    case STATUS_SUCCESS: +                        mBytesSoFar += info.mCurrentBytes - startingCount; +                        db.updateMetadata(mPackageInfo.versionCode, 0); +                        continue; +                    case STATUS_FILE_DELIVERED_INCORRECTLY: +                        // we may be on a network that is returning us a web +                        // page on redirect +                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE; +                        info.mCurrentBytes = 0; +                        db.updateDownload(info); +                        setWakeWatchdog = true; +                        break; +                    case STATUS_PAUSED_BY_APP: +                        notifyStatus = IDownloaderClient.STATE_PAUSED_BY_REQUEST; +                        break; +                    case STATUS_WAITING_FOR_NETWORK: +                    case STATUS_WAITING_TO_RETRY: +                        notifyStatus = IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE; +                        setWakeWatchdog = true; +                        break; +                    case STATUS_QUEUED_FOR_WIFI_OR_CELLULAR_PERMISSION: +                    case STATUS_QUEUED_FOR_WIFI: +                        // look for more detail here +                        if (null != mWifiManager) { +                            if (!mWifiManager.isWifiEnabled()) { +                                notifyStatus = IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION; +                                setWakeWatchdog = true; +                                break; +                            } +                        } +                        notifyStatus = IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION; +                        setWakeWatchdog = true; +                        break; +                    case STATUS_CANCELED: +                        notifyStatus = IDownloaderClient.STATE_FAILED_CANCELED; +                        setWakeWatchdog = true; +                        break; + +                    case STATUS_INSUFFICIENT_SPACE_ERROR: +                        notifyStatus = IDownloaderClient.STATE_FAILED_SDCARD_FULL; +                        setWakeWatchdog = true; +                        break; + +                    case STATUS_DEVICE_NOT_FOUND_ERROR: +                        notifyStatus = IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE; +                        setWakeWatchdog = true; +                        break; + +                    default: +                        notifyStatus = IDownloaderClient.STATE_FAILED; +                        break; +                } +                if (setWakeWatchdog) { +                    scheduleAlarm(Constants.WATCHDOG_WAKE_TIMER); +                } else { +                    cancelAlarms(); +                } +                // failure or pause state +                mNotification.onDownloadStateChanged(notifyStatus); +                return; +            } + +            // all downloads complete +            mNotification.onDownloadStateChanged(IDownloaderClient.STATE_COMPLETED); +        } finally { +            setServiceRunning(false); +        } +    } + +    @Override +    public void onDestroy() { +        if (null != mConnReceiver) { +            unregisterReceiver(mConnReceiver); +            mConnReceiver = null; +        } +        mServiceStub.disconnect(this); +        super.onDestroy(); +    } + +    public int getNetworkAvailabilityState(DownloadsDB db) { +        if (mIsConnected) { +            if (!mIsCellularConnection) +                return NETWORK_OK; +            int flags = db.mFlags; +            if (mIsRoaming) +                return NETWORK_CANNOT_USE_ROAMING; +            if (0 != (flags & FLAGS_DOWNLOAD_OVER_CELLULAR)) { +                return NETWORK_OK; +            } else { +                return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; +            } +        } +        return NETWORK_NO_CONNECTION; +    } + +    @Override +    public void onCreate() { +        super.onCreate(); +        try { +            mPackageInfo = getPackageManager().getPackageInfo( +                    getPackageName(), 0); +            ApplicationInfo ai = getApplicationInfo(); +            CharSequence applicationLabel = getPackageManager().getApplicationLabel(ai); +            mNotification = new DownloadNotification(this, applicationLabel); + +        } catch (NameNotFoundException e) { +            e.printStackTrace(); +        } +    } + +    /** +     * Exception thrown from methods called by generateSaveFile() for any fatal +     * error. +     */ +    public static class GenerateSaveFileError extends Exception { +        private static final long serialVersionUID = 3465966015408936540L; +        int mStatus; +        String mMessage; + +        public GenerateSaveFileError(int status, String message) { +            mStatus = status; +            mMessage = message; +        } +    } + +    /** +     * Returns the filename (where the file should be saved) from info about a +     * download +     */ +    public String generateTempSaveFileName(String fileName) { +        String path = Helpers.getSaveFilePath(this) +                + File.separator + fileName + TEMP_EXT; +        return path; +    } + +    /** +     * Creates a filename (where the file should be saved) from info about a +     * download. +     */ +    public String generateSaveFile(String filename, long filesize) +            throws GenerateSaveFileError { +        String path = generateTempSaveFileName(filename); +        File expPath = new File(path); +        if (!Helpers.isExternalMediaMounted()) { +            Log.d(Constants.TAG, "External media not mounted: " + path); +            throw new GenerateSaveFileError(STATUS_DEVICE_NOT_FOUND_ERROR, +                    "external media is not yet mounted"); + +        } +        if (expPath.exists()) { +            Log.d(Constants.TAG, "File already exists: " + path); +            throw new GenerateSaveFileError(STATUS_FILE_ALREADY_EXISTS_ERROR, +                    "requested destination file already exists"); +        } +        if (Helpers.getAvailableBytes(Helpers.getFilesystemRoot(path)) < filesize) { +            throw new GenerateSaveFileError(STATUS_INSUFFICIENT_SPACE_ERROR, +                    "insufficient space on external storage"); +        } +        return path; +    } + +    /** +     * @return a non-localized string appropriate for logging corresponding to +     *         one of the NETWORK_* constants. +     */ +    public String getLogMessageForNetworkError(int networkError) { +        switch (networkError) { +            case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: +                return "download size exceeds recommended limit for mobile network"; + +            case NETWORK_UNUSABLE_DUE_TO_SIZE: +                return "download size exceeds limit for mobile network"; + +            case NETWORK_NO_CONNECTION: +                return "no network connection available"; + +            case NETWORK_CANNOT_USE_ROAMING: +                return "download cannot use the current network connection because it is roaming"; + +            case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: +                return "download was requested to not use the current network type"; + +            default: +                return "unknown error with network connectivity"; +        } +    } + +    public int getControl() { +        return mControl; +    } + +    public int getStatus() { +        return mStatus; +    } + +    /** +     * Calculating a moving average for the speed so we don't get jumpy +     * calculations for time etc. +     */ +    static private final float SMOOTHING_FACTOR = 0.005f; + +    public void notifyUpdateBytes(long totalBytesSoFar) { +        long timeRemaining; +        long currentTime = SystemClock.uptimeMillis(); +        if (0 != mMillisecondsAtSample) { +            // we have a sample. +            long timePassed = currentTime - mMillisecondsAtSample; +            long bytesInSample = totalBytesSoFar - mBytesAtSample; +            float currentSpeedSample = (float) bytesInSample / (float) timePassed; +            if (0 != mAverageDownloadSpeed) { +                mAverageDownloadSpeed = SMOOTHING_FACTOR * currentSpeedSample +                        + (1 - SMOOTHING_FACTOR) * mAverageDownloadSpeed; +            } else { +                mAverageDownloadSpeed = currentSpeedSample; +            } +            timeRemaining = (long) ((mTotalLength - totalBytesSoFar) / mAverageDownloadSpeed); +        } else { +            timeRemaining = -1; +        } +        mMillisecondsAtSample = currentTime; +        mBytesAtSample = totalBytesSoFar; +        mNotification.onDownloadProgress( +                new DownloadProgressInfo(mTotalLength, +                        totalBytesSoFar, +                        timeRemaining, +                        mAverageDownloadSpeed) +                ); + +    } + +    @Override +    protected boolean shouldStop() { +        // the database automatically reads the metadata for version code +        // and download status when the instance is created +        DownloadsDB db = DownloadsDB.getDB(this); +        if (db.mStatus == 0) { +            return true; +        } +        return false; +    } + +    @Override +    public void requestDownloadStatus() { +        mNotification.resendState(); +    } + +    @Override +    public void onClientUpdated(Messenger clientMessenger) { +        this.mClientMessenger = clientMessenger; +        mNotification.setMessenger(mClientMessenger); +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java new file mode 100644 index 0000000000..c658b4cc43 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/DownloadsDB.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.provider.BaseColumns; +import android.util.Log; + +public class DownloadsDB { +    private static final String DATABASE_NAME = "DownloadsDB"; +    private static final int DATABASE_VERSION = 7; +    public static final String LOG_TAG = DownloadsDB.class.getName(); +    final SQLiteOpenHelper mHelper; +    SQLiteStatement mGetDownloadByIndex; +    SQLiteStatement mUpdateCurrentBytes; +    private static DownloadsDB mDownloadsDB; +    long mMetadataRowID = -1; +    int mVersionCode = -1; +    int mStatus = -1; +    int mFlags; + +    static public synchronized DownloadsDB getDB(Context paramContext) { +        if (null == mDownloadsDB) { +            return new DownloadsDB(paramContext); +        } +        return mDownloadsDB; +    } + +    private SQLiteStatement getDownloadByIndexStatement() { +        if (null == mGetDownloadByIndex) { +            mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( +                    "SELECT " + BaseColumns._ID + " FROM " +                            + DownloadColumns.TABLE_NAME + " WHERE " +                            + DownloadColumns.INDEX + " = ?"); +        } +        return mGetDownloadByIndex; +    } + +    private SQLiteStatement getUpdateCurrentBytesStatement() { +        if (null == mUpdateCurrentBytes) { +            mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( +                    "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES +                            + " = ?" + +                            " WHERE " + DownloadColumns.INDEX + " = ?"); +        } +        return mUpdateCurrentBytes; +    } + +    private DownloadsDB(Context paramContext) { +        this.mHelper = new DownloadsContentDBHelper(paramContext); +        final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); +        // Query for the version code, the row ID of the metadata (for future +        // updating) the status and the flags +        Cursor cur = sqldb.rawQuery("SELECT " + +                MetadataColumns.APKVERSION + "," + +                BaseColumns._ID + "," + +                MetadataColumns.DOWNLOAD_STATUS + "," + +                MetadataColumns.FLAGS + +                " FROM " +                + MetadataColumns.TABLE_NAME + " LIMIT 1", null); +        if (null != cur && cur.moveToFirst()) { +            mVersionCode = cur.getInt(0); +            mMetadataRowID = cur.getLong(1); +            mStatus = cur.getInt(2); +            mFlags = cur.getInt(3); +            cur.close(); +        } +        mDownloadsDB = this; +    } + +    protected DownloadInfo getDownloadInfoByFileName(String fileName) { +        final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); +        Cursor itemcur = null; +        try { +            itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, +                    DownloadColumns.FILENAME + " = ?", +                    new String[] { +                        fileName +                    }, null, null, null); +            if (null != itemcur && itemcur.moveToFirst()) { +                return getDownloadInfoFromCursor(itemcur); +            } +        } finally { +            if (null != itemcur) +                itemcur.close(); +        } +        return null; +    } + +    public long getIDForDownloadInfo(final DownloadInfo di) { +        return getIDByIndex(di.mIndex); +    } + +    public long getIDByIndex(int index) { +        SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); +        downloadByIndex.clearBindings(); +        downloadByIndex.bindLong(1, index); +        try { +            return downloadByIndex.simpleQueryForLong(); +        } catch (SQLiteDoneException e) { +            return -1; +        } +    } + +    public void updateDownloadCurrentBytes(final DownloadInfo di) { +        SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); +        downloadCurrentBytes.clearBindings(); +        downloadCurrentBytes.bindLong(1, di.mCurrentBytes); +        downloadCurrentBytes.bindLong(2, di.mIndex); +        downloadCurrentBytes.execute(); +    } + +    public void close() { +        this.mHelper.close(); +    } + +    protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { +        DownloadsContentDBHelper(Context paramContext) { +            super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); +        } + +        private String createTableQueryFromArray(String paramString, +                String[][] paramArrayOfString) { +            StringBuilder localStringBuilder = new StringBuilder(); +            localStringBuilder.append("CREATE TABLE "); +            localStringBuilder.append(paramString); +            localStringBuilder.append(" ("); +            int i = paramArrayOfString.length; +            for (int j = 0;; j++) { +                if (j >= i) { +                    localStringBuilder +                            .setLength(localStringBuilder.length() - 1); +                    localStringBuilder.append(");"); +                    return localStringBuilder.toString(); +                } +                String[] arrayOfString = paramArrayOfString[j]; +                localStringBuilder.append(' '); +                localStringBuilder.append(arrayOfString[0]); +                localStringBuilder.append(' '); +                localStringBuilder.append(arrayOfString[1]); +                localStringBuilder.append(','); +            } +        } + +        /** +         * These two arrays must match and have the same order. For every Schema +         * there must be a corresponding table name. +         */ +        static final private String[][][] sSchemas = { +                DownloadColumns.SCHEMA, MetadataColumns.SCHEMA +        }; + +        static final private String[] sTables = { +                DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME +        }; + +        /** +         * Goes through all of the tables in sTables and drops each table if it +         * exists. Altered to no longer make use of reflection. +         */ +        private void dropTables(SQLiteDatabase paramSQLiteDatabase) { +            for (String table : sTables) { +                try { +                    paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); +                } catch (Exception localException) { +                    localException.printStackTrace(); +                } +            } +        } + +        /** +         * Goes through all of the tables in sTables and creates a database with +         * the corresponding schema described in sSchemas. Altered to no longer +         * make use of reflection. +         */ +        public void onCreate(SQLiteDatabase paramSQLiteDatabase) { +            int numSchemas = sSchemas.length; +            for (int i = 0; i < numSchemas; i++) { +                try { +                    String[][] schema = (String[][]) sSchemas[i]; +                    paramSQLiteDatabase.execSQL(createTableQueryFromArray( +                            sTables[i], schema)); +                } catch (Exception localException) { +                    while (true) +                        localException.printStackTrace(); +                } +            } +        } + +        public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, +                int paramInt1, int paramInt2) { +            Log.w(DownloadsContentDBHelper.class.getName(), +                    "Upgrading database from version " + paramInt1 + " to " +                            + paramInt2 + ", which will destroy all old data"); +            dropTables(paramSQLiteDatabase); +            onCreate(paramSQLiteDatabase); +        } +    } + +    public static class MetadataColumns implements BaseColumns { +        public static final String APKVERSION = "APKVERSION"; +        public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; +        public static final String FLAGS = "DOWNLOADFLAGS"; + +        public static final String[][] SCHEMA = { +                { +                        BaseColumns._ID, "INTEGER PRIMARY KEY" +                }, +                { +                        APKVERSION, "INTEGER" +                }, { +                        DOWNLOAD_STATUS, "INTEGER" +                }, +                { +                        FLAGS, "INTEGER" +                } +        }; +        public static final String TABLE_NAME = "MetadataColumns"; +        public static final String _ID = "MetadataColumns._id"; +    } + +    public static class DownloadColumns implements BaseColumns { +        public static final String INDEX = "FILEIDX"; +        public static final String URI = "URI"; +        public static final String FILENAME = "FN"; +        public static final String ETAG = "ETAG"; + +        public static final String TOTALBYTES = "TOTALBYTES"; +        public static final String CURRENTBYTES = "CURRENTBYTES"; +        public static final String LASTMOD = "LASTMOD"; + +        public static final String STATUS = "STATUS"; +        public static final String CONTROL = "CONTROL"; +        public static final String NUM_FAILED = "FAILCOUNT"; +        public static final String RETRY_AFTER = "RETRYAFTER"; +        public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; + +        public static final String[][] SCHEMA = { +                { +                        BaseColumns._ID, "INTEGER PRIMARY KEY" +                }, +                { +                        INDEX, "INTEGER UNIQUE" +                }, { +                        URI, "TEXT" +                }, +                { +                        FILENAME, "TEXT UNIQUE" +                }, { +                        ETAG, "TEXT" +                }, +                { +                        TOTALBYTES, "INTEGER" +                }, { +                        CURRENTBYTES, "INTEGER" +                }, +                { +                        LASTMOD, "INTEGER" +                }, { +                        STATUS, "INTEGER" +                }, +                { +                        CONTROL, "INTEGER" +                }, { +                        NUM_FAILED, "INTEGER" +                }, +                { +                        RETRY_AFTER, "INTEGER" +                }, { +                        REDIRECT_COUNT, "INTEGER" +                } +        }; +        public static final String TABLE_NAME = "DownloadColumns"; +        public static final String _ID = "DownloadColumns._id"; +    } + +    private static final String[] DC_PROJECTION = { +            DownloadColumns.FILENAME, +            DownloadColumns.URI, DownloadColumns.ETAG, +            DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, +            DownloadColumns.LASTMOD, DownloadColumns.STATUS, +            DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, +            DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, +            DownloadColumns.INDEX +    }; + +    private static final int FILENAME_IDX = 0; +    private static final int URI_IDX = 1; +    private static final int ETAG_IDX = 2; +    private static final int TOTALBYTES_IDX = 3; +    private static final int CURRENTBYTES_IDX = 4; +    private static final int LASTMOD_IDX = 5; +    private static final int STATUS_IDX = 6; +    private static final int CONTROL_IDX = 7; +    private static final int NUM_FAILED_IDX = 8; +    private static final int RETRY_AFTER_IDX = 9; +    private static final int REDIRECT_COUNT_IDX = 10; +    private static final int INDEX_IDX = 11; + +    /** +     * This function will add a new file to the database if it does not exist. +     * +     * @param di DownloadInfo that we wish to store +     * @return the row id of the record to be updated/inserted, or -1 +     */ +    public boolean updateDownload(DownloadInfo di) { +        ContentValues cv = new ContentValues(); +        cv.put(DownloadColumns.INDEX, di.mIndex); +        cv.put(DownloadColumns.FILENAME, di.mFileName); +        cv.put(DownloadColumns.URI, di.mUri); +        cv.put(DownloadColumns.ETAG, di.mETag); +        cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); +        cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); +        cv.put(DownloadColumns.LASTMOD, di.mLastMod); +        cv.put(DownloadColumns.STATUS, di.mStatus); +        cv.put(DownloadColumns.CONTROL, di.mControl); +        cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); +        cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); +        cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); +        return updateDownload(di, cv); +    } + +    public boolean updateDownload(DownloadInfo di, ContentValues cv) { +        long id = di == null ? -1 : getIDForDownloadInfo(di); +        try { +            final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); +            if (id != -1) { +                if (1 != sqldb.update(DownloadColumns.TABLE_NAME, +                        cv, DownloadColumns._ID + " = " + id, null)) { +                    return false; +                } +            } else { +                return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, +                        DownloadColumns.URI, cv); +            } +        } catch (android.database.sqlite.SQLiteException ex) { +            ex.printStackTrace(); +        } +        return false; +    } + +    public int getLastCheckedVersionCode() { +        return mVersionCode; +    } + +    public boolean isDownloadRequired() { +        final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); +        Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " +                + DownloadColumns.TABLE_NAME + " WHERE " +                + DownloadColumns.STATUS + " <> 0", null); +        try { +            if (null != cur && cur.moveToFirst()) { +                return 0 == cur.getInt(0); +            } +        } finally { +            if (null != cur) +                cur.close(); +        } +        return true; +    } + +    public int getFlags() { +        return mFlags; +    } + +    public boolean updateFlags(int flags) { +        if (mFlags != flags) { +            ContentValues cv = new ContentValues(); +            cv.put(MetadataColumns.FLAGS, flags); +            if (updateMetadata(cv)) { +                mFlags = flags; +                return true; +            } else { +                return false; +            } +        } else { +            return true; +        } +    }; + +    public boolean updateStatus(int status) { +        if (mStatus != status) { +            ContentValues cv = new ContentValues(); +            cv.put(MetadataColumns.DOWNLOAD_STATUS, status); +            if (updateMetadata(cv)) { +                mStatus = status; +                return true; +            } else { +                return false; +            } +        } else { +            return true; +        } +    }; + +    public boolean updateMetadata(ContentValues cv) { +        final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); +        if (-1 == this.mMetadataRowID) { +            long newID = sqldb.insert(MetadataColumns.TABLE_NAME, +                    MetadataColumns.APKVERSION, cv); +            if (-1 == newID) +                return false; +            mMetadataRowID = newID; +        } else { +            if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, +                    BaseColumns._ID + " = " + mMetadataRowID, null)) +                return false; +        } +        return true; +    } + +    public boolean updateMetadata(int apkVersion, int downloadStatus) { +        ContentValues cv = new ContentValues(); +        cv.put(MetadataColumns.APKVERSION, apkVersion); +        cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); +        if (updateMetadata(cv)) { +            mVersionCode = apkVersion; +            mStatus = downloadStatus; +            return true; +        } else { +            return false; +        } +    }; + +    public boolean updateFromDb(DownloadInfo di) { +        final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); +        Cursor cur = null; +        try { +            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, +                    DownloadColumns.FILENAME + "= ?", +                    new String[] { +                        di.mFileName +                    }, null, null, null); +            if (null != cur && cur.moveToFirst()) { +                setDownloadInfoFromCursor(di, cur); +                return true; +            } +            return false; +        } finally { +            if (null != cur) { +                cur.close(); +            } +        } +    } + +    public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { +        di.mUri = cur.getString(URI_IDX); +        di.mETag = cur.getString(ETAG_IDX); +        di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); +        di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); +        di.mLastMod = cur.getLong(LASTMOD_IDX); +        di.mStatus = cur.getInt(STATUS_IDX); +        di.mControl = cur.getInt(CONTROL_IDX); +        di.mNumFailed = cur.getInt(NUM_FAILED_IDX); +        di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); +        di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); +    } + +    public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { +        DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), +                cur.getString(FILENAME_IDX), this.getClass().getPackage() +                        .getName()); +        setDownloadInfoFromCursor(di, cur); +        return di; +    } + +    public DownloadInfo[] getDownloads() { +        final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); +        Cursor cur = null; +        try { +            cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, +                    null, null, null, null); +            if (null != cur && cur.moveToFirst()) { +                DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; +                int idx = 0; +                do { +                    DownloadInfo di = getDownloadInfoFromCursor(cur); +                    retInfos[idx++] = di; +                } while (cur.moveToNext()); +                return retInfos; +            } +            return null; +        } finally { +            if (null != cur) { +                cur.close(); +            } +        } +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java new file mode 100644 index 0000000000..3f440e9893 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/expansion/downloader/impl/HttpDateTime.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.expansion.downloader.impl; + +import android.text.format.Time; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper for parsing an HTTP date. + */ +public final class HttpDateTime { + +    /* +     * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT +     * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850, +     * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format +     * with following variations Wdy, DD-Mon-YYYY HH:MM:SS GMT Wdy, (SP)D Mon +     * YYYY HH:MM:SS GMT Wdy,DD Mon YYYY HH:MM:SS GMT Wdy, DD-Mon-YY HH:MM:SS +     * GMT Wdy, DD Mon YYYY HH:MM:SS -HHMM Wdy, DD Mon YYYY HH:MM:SS Wdy Mon +     * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first +     * digit is zero. Mon can be the full name of the month. +     */ +    private static final String HTTP_DATE_RFC_REGEXP = +            "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" +                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; + +    private static final String HTTP_DATE_ANSIC_REGEXP = +            "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" +                    + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + +    /** +     * The compiled version of the HTTP-date regular expressions. +     */ +    private static final Pattern HTTP_DATE_RFC_PATTERN = +            Pattern.compile(HTTP_DATE_RFC_REGEXP); +    private static final Pattern HTTP_DATE_ANSIC_PATTERN = +            Pattern.compile(HTTP_DATE_ANSIC_REGEXP); + +    private static class TimeOfDay { +        TimeOfDay(int h, int m, int s) { +            this.hour = h; +            this.minute = m; +            this.second = s; +        } + +        int hour; +        int minute; +        int second; +    } + +    public static long parse(String timeString) +            throws IllegalArgumentException { + +        int date = 1; +        int month = Calendar.JANUARY; +        int year = 1970; +        TimeOfDay timeOfDay; + +        Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); +        if (rfcMatcher.find()) { +            date = getDate(rfcMatcher.group(1)); +            month = getMonth(rfcMatcher.group(2)); +            year = getYear(rfcMatcher.group(3)); +            timeOfDay = getTime(rfcMatcher.group(4)); +        } else { +            Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); +            if (ansicMatcher.find()) { +                month = getMonth(ansicMatcher.group(1)); +                date = getDate(ansicMatcher.group(2)); +                timeOfDay = getTime(ansicMatcher.group(3)); +                year = getYear(ansicMatcher.group(4)); +            } else { +                throw new IllegalArgumentException(); +            } +        } + +        // FIXME: Y2038 BUG! +        if (year >= 2038) { +            year = 2038; +            month = Calendar.JANUARY; +            date = 1; +        } + +        Time time = new Time(Time.TIMEZONE_UTC); +        time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, +                month, year); +        return time.toMillis(false /* use isDst */); +    } + +    private static int getDate(String dateString) { +        if (dateString.length() == 2) { +            return (dateString.charAt(0) - '0') * 10 +                    + (dateString.charAt(1) - '0'); +        } else { +            return (dateString.charAt(0) - '0'); +        } +    } + +    /* +     * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0 +     * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20 +     * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19 +     * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9 +     */ +    private static int getMonth(String monthString) { +        int hash = Character.toLowerCase(monthString.charAt(0)) + +                Character.toLowerCase(monthString.charAt(1)) + +                Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; +        switch (hash) { +            case 22: +                return Calendar.JANUARY; +            case 10: +                return Calendar.FEBRUARY; +            case 29: +                return Calendar.MARCH; +            case 32: +                return Calendar.APRIL; +            case 36: +                return Calendar.MAY; +            case 42: +                return Calendar.JUNE; +            case 40: +                return Calendar.JULY; +            case 26: +                return Calendar.AUGUST; +            case 37: +                return Calendar.SEPTEMBER; +            case 35: +                return Calendar.OCTOBER; +            case 48: +                return Calendar.NOVEMBER; +            case 9: +                return Calendar.DECEMBER; +            default: +                throw new IllegalArgumentException(); +        } +    } + +    private static int getYear(String yearString) { +        if (yearString.length() == 2) { +            int year = (yearString.charAt(0) - '0') * 10 +                    + (yearString.charAt(1) - '0'); +            if (year >= 70) { +                return year + 1900; +            } else { +                return year + 2000; +            } +        } else if (yearString.length() == 3) { +            // According to RFC 2822, three digit years should be added to 1900. +            int year = (yearString.charAt(0) - '0') * 100 +                    + (yearString.charAt(1) - '0') * 10 +                    + (yearString.charAt(2) - '0'); +            return year + 1900; +        } else if (yearString.length() == 4) { +            return (yearString.charAt(0) - '0') * 1000 +                    + (yearString.charAt(1) - '0') * 100 +                    + (yearString.charAt(2) - '0') * 10 +                    + (yearString.charAt(3) - '0'); +        } else { +            return 1970; +        } +    } + +    private static TimeOfDay getTime(String timeString) { +        // HH might be H +        int i = 0; +        int hour = timeString.charAt(i++) - '0'; +        if (timeString.charAt(i) != ':') +            hour = hour * 10 + (timeString.charAt(i++) - '0'); +        // Skip ':' +        i++; + +        int minute = (timeString.charAt(i++) - '0') * 10 +                + (timeString.charAt(i++) - '0'); +        // Skip ':' +        i++; + +        int second = (timeString.charAt(i++) - '0') * 10 +                + (timeString.charAt(i++) - '0'); + +        return new TimeOfDay(hour, minute, second); +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java new file mode 100644 index 0000000000..d6ccb0c5e4 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/AESObfuscator.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * An Obfuscator that uses AES to encrypt data. + */ +public class AESObfuscator implements Obfuscator { +    private static final String UTF8 = "UTF-8"; +    private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; +    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; +    private static final byte[] IV = +        { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; +    private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; + +    private Cipher mEncryptor; +    private Cipher mDecryptor; + +    /** +     * @param salt an array of random bytes to use for each (un)obfuscation +     * @param applicationId application identifier, e.g. the package name +     * @param deviceId device identifier. Use as many sources as possible to +     *    create this unique identifier. +     */ +    public AESObfuscator(byte[] salt, String applicationId, String deviceId) { +        try { +            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); +            KeySpec keySpec = +                new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); +            SecretKey tmp = factory.generateSecret(keySpec); +            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); +            mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); +            mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); +            mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); +            mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); +        } catch (GeneralSecurityException e) { +            // This can't happen on a compatible Android device. +            throw new RuntimeException("Invalid environment", e); +        } +    } + +    public String obfuscate(String original, String key) { +        if (original == null) { +            return null; +        } +        try { +            // Header is appended as an integrity check +            return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); +        } catch (UnsupportedEncodingException e) { +            throw new RuntimeException("Invalid environment", e); +        } catch (GeneralSecurityException e) { +            throw new RuntimeException("Invalid environment", e); +        } +    } + +    public String unobfuscate(String obfuscated, String key) throws ValidationException { +        if (obfuscated == null) { +            return null; +        } +        try { +            String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); +            // Check for presence of header. This serves as a final integrity check, for cases +            // where the block size is correct during decryption. +            int headerIndex = result.indexOf(header+key); +            if (headerIndex != 0) { +                throw new ValidationException("Header not found (invalid data or key)" + ":" + +                        obfuscated); +            } +            return result.substring(header.length()+key.length(), result.length()); +        } catch (Base64DecoderException e) { +            throw new ValidationException(e.getMessage() + ":" + obfuscated); +        } catch (IllegalBlockSizeException e) { +            throw new ValidationException(e.getMessage() + ":" + obfuscated); +        } catch (BadPaddingException e) { +            throw new ValidationException(e.getMessage() + ":" + obfuscated); +        } catch (UnsupportedEncodingException e) { +            throw new RuntimeException("Invalid environment", e); +        } +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000000..37fad8926a --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -0,0 +1,414 @@ + +package com.google.android.vending.licensing; + +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.vending.licensing.util.URIQueryDecoder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +/** + * Default policy. All policy decisions are based off of response data received + * from the licensing service. Specifically, the licensing server sends the + * following information: response validity period, error retry period, + * error retry count and a URL for restoring app access in unlicensed cases. + * <p> + * These values will vary based on the the way the application is configured in + * the Google Play publishing console, such as whether the application is + * marked as free or is within its refund period, as well as how often an + * application is checking with the licensing service. + * <p> + * Developers who need more fine grained control over their application's + * licensing policy should implement a custom Policy. + */ +public class APKExpansionPolicy implements Policy { + +    private static final String TAG = "APKExpansionPolicy"; +    private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy"; +    private static final String PREF_LAST_RESPONSE = "lastResponse"; +    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; +    private static final String PREF_RETRY_UNTIL = "retryUntil"; +    private static final String PREF_MAX_RETRIES = "maxRetries"; +    private static final String PREF_RETRY_COUNT = "retryCount"; +    private static final String PREF_LICENSING_URL = "licensingUrl"; +    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; +    private static final String DEFAULT_RETRY_UNTIL = "0"; +    private static final String DEFAULT_MAX_RETRIES = "0"; +    private static final String DEFAULT_RETRY_COUNT = "0"; + +    private static final long MILLIS_PER_MINUTE = 60 * 1000; + +    private long mValidityTimestamp; +    private long mRetryUntil; +    private long mMaxRetries; +    private long mRetryCount; +    private long mLastResponseTime = 0; +    private int mLastResponse; +    private String mLicensingUrl; +    private PreferenceObfuscator mPreferences; +    private Vector<String> mExpansionURLs = new Vector<String>(); +    private Vector<String> mExpansionFileNames = new Vector<String>(); +    private Vector<Long> mExpansionFileSizes = new Vector<Long>(); + +    /** +     * The design of the protocol supports n files. Currently the market can +     * only deliver two files. To accommodate this, we have these two constants, +     * but the order is the only relevant thing here. +     */ +    public static final int MAIN_FILE_URL_INDEX = 0; +    public static final int PATCH_FILE_URL_INDEX = 1; + +    /** +     * @param context The context for the current application +     * @param obfuscator An obfuscator to be used with preferences. +     */ +    public APKExpansionPolicy(Context context, Obfuscator obfuscator) { +        // Import old values +        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); +        mPreferences = new PreferenceObfuscator(sp, obfuscator); +        mLastResponse = Integer.parseInt( +                mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); +        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, +                DEFAULT_VALIDITY_TIMESTAMP)); +        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); +        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); +        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); +        mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); +    } + +    /** +     * We call this to guarantee that we fetch a fresh policy from the server. +     * This is to be used if the URL is invalid. +     */ +    public void resetPolicy() { +        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); +        setRetryUntil(DEFAULT_RETRY_UNTIL); +        setMaxRetries(DEFAULT_MAX_RETRIES); +        setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); +        setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); +        mPreferences.commit(); +    } + +    /** +     * Process a new response from the license server. +     * <p> +     * This data will be used for computing future policy decisions. The +     * following parameters are processed: +     * <ul> +     * <li>VT: the timestamp that the client should consider the response valid +     * until +     * <li>GT: the timestamp that the client should ignore retry errors until +     * <li>GR: the number of retry errors that the client should ignore +     * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. +     * buy app on the Play Store) +     * </ul> +     * +     * @param response the result from validating the server response +     * @param rawData the raw server response data +     */ +    public void processServerResponse(int response, +            com.google.android.vending.licensing.ResponseData rawData) { + +        // Update retry counter +        if (response != Policy.RETRY) { +            setRetryCount(0); +        } else { +            setRetryCount(mRetryCount + 1); +        } + +        // Update server policy data +        Map<String, String> extras = decodeExtras(rawData); +        if (response == Policy.LICENSED) { +            mLastResponse = response; +            // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. +            setLicensingUrl(null); +            setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); +            Set<String> keys = extras.keySet(); +            for (String key : keys) { +                if (key.equals("VT")) { +                    setValidityTimestamp(extras.get(key)); +                } else if (key.equals("GT")) { +                    setRetryUntil(extras.get(key)); +                } else if (key.equals("GR")) { +                    setMaxRetries(extras.get(key)); +                } else if (key.startsWith("FILE_URL")) { +                    int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; +                    setExpansionURL(index, extras.get(key)); +                } else if (key.startsWith("FILE_NAME")) { +                    int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; +                    setExpansionFileName(index, extras.get(key)); +                } else if (key.startsWith("FILE_SIZE")) { +                    int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; +                    setExpansionFileSize(index, Long.parseLong(extras.get(key))); +                } +            } +        } else if (response == Policy.NOT_LICENSED) { +            // Clear out stale retry params +            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); +            setRetryUntil(DEFAULT_RETRY_UNTIL); +            setMaxRetries(DEFAULT_MAX_RETRIES); +            // Update the licensing URL +            setLicensingUrl(extras.get("LU")); +        } + +        setLastResponse(response); +        mPreferences.commit(); +    } + +    /** +     * Set the last license response received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param l the response +     */ +    private void setLastResponse(int l) { +        mLastResponseTime = System.currentTimeMillis(); +        mLastResponse = l; +        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); +    } + +    /** +     * Set the current retry count and add to preferences. You must manually +     * call PreferenceObfuscator.commit() to commit these changes to disk. +     * +     * @param c the new retry count +     */ +    private void setRetryCount(long c) { +        mRetryCount = c; +        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); +    } + +    public long getRetryCount() { +        return mRetryCount; +    } + +    /** +     * Set the last validity timestamp (VT) received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param validityTimestamp the VT string received +     */ +    private void setValidityTimestamp(String validityTimestamp) { +        Long lValidityTimestamp; +        try { +            lValidityTimestamp = Long.parseLong(validityTimestamp); +        } catch (NumberFormatException e) { +            // No response or not parseable, expire in one minute. +            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); +            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; +            validityTimestamp = Long.toString(lValidityTimestamp); +        } + +        mValidityTimestamp = lValidityTimestamp; +        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); +    } + +    public long getValidityTimestamp() { +        return mValidityTimestamp; +    } + +    /** +     * Set the retry until timestamp (GT) received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param retryUntil the GT string received +     */ +    private void setRetryUntil(String retryUntil) { +        Long lRetryUntil; +        try { +            lRetryUntil = Long.parseLong(retryUntil); +        } catch (NumberFormatException e) { +            // No response or not parseable, expire immediately +            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); +            retryUntil = "0"; +            lRetryUntil = 0l; +        } + +        mRetryUntil = lRetryUntil; +        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); +    } + +    public long getRetryUntil() { +        return mRetryUntil; +    } + +    /** +     * Set the max retries value (GR) as received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param maxRetries the GR string received +     */ +    private void setMaxRetries(String maxRetries) { +        Long lMaxRetries; +        try { +            lMaxRetries = Long.parseLong(maxRetries); +        } catch (NumberFormatException e) { +            // No response or not parseable, expire immediately +            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); +            maxRetries = "0"; +            lMaxRetries = 0l; +        } + +        mMaxRetries = lMaxRetries; +        mPreferences.putString(PREF_MAX_RETRIES, maxRetries); +    } + +    public long getMaxRetries() { +        return mMaxRetries; +    } + +    /** +     * Set the licensing URL that displays a Play Store UI for the user to regain app access. +     * +     * @param url the LU string received +     */ +    private void setLicensingUrl(String url) { +        mLicensingUrl = url; +        mPreferences.putString(PREF_LICENSING_URL, url); +    } + +    public String getLicensingUrl() { +        return mLicensingUrl; +    } + +    /** +     * Gets the count of expansion URLs. Since expansionURLs are not committed +     * to preferences, this will return zero if there has been no LVL fetch +     * in the current session. +     * +     * @return the number of expansion URLs. (0,1,2) +     */ +    public int getExpansionURLCount() { +        return mExpansionURLs.size(); +    } + +    /** +     * Gets the expansion URL. Since these URLs are not committed to +     * preferences, this will always return null if there has not been an LVL +     * fetch in the current session. +     * +     * @param index the index of the URL to fetch. This value will be either +     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX +     */ +    public String getExpansionURL(int index) { +        if (index < mExpansionURLs.size()) { +            return mExpansionURLs.elementAt(index); +        } +        return null; +    } + +    /** +     * Sets the expansion URL. Expansion URL's are not committed to preferences, +     * but are instead intended to be stored when the license response is +     * processed by the front-end. +     * +     * @param index the index of the expansion URL. This value will be either +     *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX +     * @param URL the URL to set +     */ +    public void setExpansionURL(int index, String URL) { +        if (index >= mExpansionURLs.size()) { +            mExpansionURLs.setSize(index + 1); +        } +        mExpansionURLs.set(index, URL); +    } + +    public String getExpansionFileName(int index) { +        if (index < mExpansionFileNames.size()) { +            return mExpansionFileNames.elementAt(index); +        } +        return null; +    } + +    public void setExpansionFileName(int index, String name) { +        if (index >= mExpansionFileNames.size()) { +            mExpansionFileNames.setSize(index + 1); +        } +        mExpansionFileNames.set(index, name); +    } + +    public long getExpansionFileSize(int index) { +        if (index < mExpansionFileSizes.size()) { +            return mExpansionFileSizes.elementAt(index); +        } +        return -1; +    } + +    public void setExpansionFileSize(int index, long size) { +        if (index >= mExpansionFileSizes.size()) { +            mExpansionFileSizes.setSize(index + 1); +        } +        mExpansionFileSizes.set(index, size); +    } + +    /** +     * {@inheritDoc} This implementation allows access if either:<br> +     * <ol> +     * <li>a LICENSED response was received within the validity period +     * <li>a RETRY response was received in the last minute, and we are under +     * the RETRY count or in the RETRY period. +     * </ol> +     */ +    public boolean allowAccess() { +        long ts = System.currentTimeMillis(); +        if (mLastResponse == Policy.LICENSED) { +            // Check if the LICENSED response occurred within the validity +            // timeout. +            if (ts <= mValidityTimestamp) { +                // Cached LICENSED response is still valid. +                return true; +            } +        } else if (mLastResponse == Policy.RETRY && +                ts < mLastResponseTime + MILLIS_PER_MINUTE) { +            // Only allow access if we are within the retry period or we haven't +            // used up our +            // max retries. +            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); +        } +        return false; +    } + +    private Map<String, String> decodeExtras( +        com.google.android.vending.licensing.ResponseData rawData) { +        Map<String, String> results = new HashMap<String, String>(); +        if (rawData == null) { +            return results; +        } + +        try { +            URI rawExtras = new URI("?" + rawData.extra); +            URIQueryDecoder.DecodeQuery(rawExtras, results); +        } catch (URISyntaxException e) { +            Log.w(TAG, "Invalid syntax error while decoding extras data from server."); +        } +        return results; +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java new file mode 100644 index 0000000000..e5c5e2d7ca --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/DeviceLimiter.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Allows the developer to limit the number of devices using a single license. + * <p> + * The LICENSED response from the server contains a user identifier unique to + * the <application, user> pair. The developer can send this identifier + * to their own server along with some device identifier (a random number + * generated and stored once per application installation, + * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId}, + * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc). + * The more sources used to identify the device, the harder it will be for an + * attacker to spoof. + * <p> + * The server can look at the <application, user, device id> tuple and + * restrict a user's application license to run on at most 10 different devices + * in a week (for example). We recommend not being too restrictive because a + * user might legitimately have multiple devices or be in the process of + * changing phones. This will catch egregious violations of multiple people + * sharing one license. + */ +public interface DeviceLimiter { + +    /** +     * Checks if this device is allowed to use the given user's license. +     * +     * @param userId the user whose license the server responded with +     * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs +     */ +    int isDeviceAllowed(String userId); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java new file mode 100644 index 0000000000..15017b3425 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings.Secure; +import android.util.Log; + +import com.android.vending.licensing.ILicenseResultListener; +import com.android.vending.licensing.ILicensingService; +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +/** + * Client library for Google Play license verifications. + * <p> + * The LicenseChecker is configured via a {@link Policy} which contains the logic to determine + * whether a user should have access to the application. For example, the Policy can define a + * threshold for allowable number of server or client failures before the library reports the user + * as not having access. + * <p> + * Must also provide the Base64-encoded RSA public key associated with your developer account. The + * public key is obtainable from the publisher site. + */ +public class LicenseChecker implements ServiceConnection { +    private static final String TAG = "LicenseChecker"; + +    private static final String KEY_FACTORY_ALGORITHM = "RSA"; + +    // Timeout value (in milliseconds) for calls to service. +    private static final int TIMEOUT_MS = 10 * 1000; + +    private static final SecureRandom RANDOM = new SecureRandom(); +    private static final boolean DEBUG_LICENSE_ERROR = false; + +    private ILicensingService mService; + +    private PublicKey mPublicKey; +    private final Context mContext; +    private final Policy mPolicy; +    /** +     * A handler for running tasks on a background thread. We don't want license processing to block +     * the UI thread. +     */ +    private Handler mHandler; +    private final String mPackageName; +    private final String mVersionCode; +    private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>(); +    private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>(); + +    /** +     * @param context a Context +     * @param policy implementation of Policy +     * @param encodedPublicKey Base64-encoded RSA public key +     * @throws IllegalArgumentException if encodedPublicKey is invalid +     */ +    public LicenseChecker(Context context, Policy policy, String encodedPublicKey) { +        mContext = context; +        mPolicy = policy; +        mPublicKey = generatePublicKey(encodedPublicKey); +        mPackageName = mContext.getPackageName(); +        mVersionCode = getVersionCode(context, mPackageName); +        HandlerThread handlerThread = new HandlerThread("background thread"); +        handlerThread.start(); +        mHandler = new Handler(handlerThread.getLooper()); +    } + +    /** +     * Generates a PublicKey instance from a string containing the Base64-encoded public key. +     * +     * @param encodedPublicKey Base64-encoded public key +     * @throws IllegalArgumentException if encodedPublicKey is invalid +     */ +    private static PublicKey generatePublicKey(String encodedPublicKey) { +        try { +            byte[] decodedKey = Base64.decode(encodedPublicKey); +            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + +            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); +        } catch (NoSuchAlgorithmException e) { +            // This won't happen in an Android-compatible environment. +            throw new RuntimeException(e); +        } catch (Base64DecoderException e) { +            Log.e(TAG, "Could not decode from Base64."); +            throw new IllegalArgumentException(e); +        } catch (InvalidKeySpecException e) { +            Log.e(TAG, "Invalid key specification."); +            throw new IllegalArgumentException(e); +        } +    } + +    /** +     * Checks if the user should have access to the app. Binds the service if necessary. +     * <p> +     * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we +     * recommend obfuscating the string that is passed into bindService using another method of your +     * own devising. +     * <p> +     * source string: "com.android.vending.licensing.ILicensingService" +     * <p> +     *  +     * @param callback +     */ +    public synchronized void checkAccess(LicenseCheckerCallback callback) { +        // If we have a valid recent LICENSED response, we can skip asking +        // Market. +        if (mPolicy.allowAccess()) { +            Log.i(TAG, "Using cached license response"); +            callback.allow(Policy.LICENSED); +        } else { +            LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(), +                    callback, generateNonce(), mPackageName, mVersionCode); + +            if (mService == null) { +                Log.i(TAG, "Binding to licensing service."); +                try { +                    boolean bindResult = mContext +                            .bindService( +                                    new Intent( +                                            new String( +                                                    // Base64 encoded - +                                                    // com.android.vending.licensing.ILicensingService +                                                    // Consider encoding this in another way in your +                                                    // code to improve security +                                                    Base64.decode( +                                                            "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))) +                                                                    // As of Android 5.0, implicit +                                                                    // Service Intents are no longer +                                                                    // allowed because it's not +                                                                    // possible for the user to +                                                                    // participate in disambiguating +                                                                    // them. This does mean we break +                                                                    // compatibility with Android +                                                                    // Cupcake devices with this +                                                                    // release, since setPackage was +                                                                    // added in Donut. +                                                                    .setPackage( +                                                                            new String( +                                                                                    // Base64 +                                                                                    // encoded - +                                                                                    // com.android.vending +                                                                                    Base64.decode( +                                                                                            "Y29tLmFuZHJvaWQudmVuZGluZw=="))), +                                    this, // ServiceConnection. +                                    Context.BIND_AUTO_CREATE); +                    if (bindResult) { +                        mPendingChecks.offer(validator); +                    } else { +                        Log.e(TAG, "Could not bind to service."); +                        handleServiceConnectionError(validator); +                    } +                } catch (SecurityException e) { +                    callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION); +                } catch (Base64DecoderException e) { +                    e.printStackTrace(); +                } +            } else { +                mPendingChecks.offer(validator); +                runChecks(); +            } +        } +    } + +    /** +     * Triggers the last deep link licensing URL returned from the server, which redirects users to a +     * page which enables them to gain access to the app. If no such URL is returned by the server, it +     * will go to the details page of the app in the Play Store. +     */ +    public void followLastLicensingUrl(Context context) { +        String licensingUrl = mPolicy.getLicensingUrl(); +        if (licensingUrl == null) { +            licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName(); +        } +        Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl)); +        context.startActivity(marketIntent); +    } + +    private void runChecks() { +        LicenseValidator validator; +        while ((validator = mPendingChecks.poll()) != null) { +            try { +                Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); +                mService.checkLicense( +                        validator.getNonce(), validator.getPackageName(), +                        new ResultListener(validator)); +                mChecksInProgress.add(validator); +            } catch (RemoteException e) { +                Log.w(TAG, "RemoteException in checkLicense call.", e); +                handleServiceConnectionError(validator); +            } +        } +    } + +    private synchronized void finishCheck(LicenseValidator validator) { +        mChecksInProgress.remove(validator); +        if (mChecksInProgress.isEmpty()) { +            cleanupService(); +        } +    } + +    private class ResultListener extends ILicenseResultListener.Stub { +        private final LicenseValidator mValidator; +        private Runnable mOnTimeout; + +        public ResultListener(LicenseValidator validator) { +            mValidator = validator; +            mOnTimeout = new Runnable() { +                public void run() { +                    Log.i(TAG, "Check timed out."); +                    handleServiceConnectionError(mValidator); +                    finishCheck(mValidator); +                } +            }; +            startTimeout(); +        } + +        private static final int ERROR_CONTACTING_SERVER = 0x101; +        private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; +        private static final int ERROR_NON_MATCHING_UID = 0x103; + +        // Runs in IPC thread pool. Post it to the Handler, so we can guarantee +        // either this or the timeout runs. +        public void verifyLicense(final int responseCode, final String signedData, +                final String signature) { +            mHandler.post(new Runnable() { +                public void run() { +                    Log.i(TAG, "Received response."); +                    // Make sure it hasn't already timed out. +                    if (mChecksInProgress.contains(mValidator)) { +                        clearTimeout(); +                        mValidator.verify(mPublicKey, responseCode, signedData, signature); +                        finishCheck(mValidator); +                    } +                    if (DEBUG_LICENSE_ERROR) { +                        boolean logResponse; +                        String stringError = null; +                        switch (responseCode) { +                            case ERROR_CONTACTING_SERVER: +                                logResponse = true; +                                stringError = "ERROR_CONTACTING_SERVER"; +                                break; +                            case ERROR_INVALID_PACKAGE_NAME: +                                logResponse = true; +                                stringError = "ERROR_INVALID_PACKAGE_NAME"; +                                break; +                            case ERROR_NON_MATCHING_UID: +                                logResponse = true; +                                stringError = "ERROR_NON_MATCHING_UID"; +                                break; +                            default: +                                logResponse = false; +                        } + +                        if (logResponse) { +                            String android_id = Secure.getString(mContext.getContentResolver(), +                                    Secure.ANDROID_ID); +                            Date date = new Date(); +                            Log.d(TAG, "Server Failure: " + stringError); +                            Log.d(TAG, "Android ID: " + android_id); +                            Log.d(TAG, "Time: " + date.toGMTString()); +                        } +                    } + +                } +            }); +        } + +        private void startTimeout() { +            Log.i(TAG, "Start monitoring timeout."); +            mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); +        } + +        private void clearTimeout() { +            Log.i(TAG, "Clearing timeout."); +            mHandler.removeCallbacks(mOnTimeout); +        } +    } + +    public synchronized void onServiceConnected(ComponentName name, IBinder service) { +        mService = ILicensingService.Stub.asInterface(service); +        runChecks(); +    } + +    public synchronized void onServiceDisconnected(ComponentName name) { +        // Called when the connection with the service has been +        // unexpectedly disconnected. That is, Market crashed. +        // If there are any checks in progress, the timeouts will handle them. +        Log.w(TAG, "Service unexpectedly disconnected."); +        mService = null; +    } + +    /** +     * Generates policy response for service connection errors, as a result of disconnections or +     * timeouts. +     */ +    private synchronized void handleServiceConnectionError(LicenseValidator validator) { +        mPolicy.processServerResponse(Policy.RETRY, null); + +        if (mPolicy.allowAccess()) { +            validator.getCallback().allow(Policy.RETRY); +        } else { +            validator.getCallback().dontAllow(Policy.RETRY); +        } +    } + +    /** Unbinds service if necessary and removes reference to it. */ +    private void cleanupService() { +        if (mService != null) { +            try { +                mContext.unbindService(this); +            } catch (IllegalArgumentException e) { +                // Somehow we've already been unbound. This is a non-fatal +                // error. +                Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); +            } +            mService = null; +        } +    } + +    /** +     * Inform the library that the context is about to be destroyed, so that any open connections +     * can be cleaned up. +     * <p> +     * Failure to call this method can result in a crash under certain circumstances, such as during +     * screen rotation if an Activity requests the license check or when the user exits the +     * application. +     */ +    public synchronized void onDestroy() { +        cleanupService(); +        mHandler.getLooper().quit(); +    } + +    /** Generates a nonce (number used once). */ +    private int generateNonce() { +        return RANDOM.nextInt(); +    } + +    /** +     * Get version code for the application package name. +     * +     * @param context +     * @param packageName application package name +     * @return the version code or empty string if package not found +     */ +    private static String getVersionCode(Context context, String packageName) { +        try { +            return String.valueOf( +                    context.getPackageManager().getPackageInfo(packageName, 0).versionCode); +        } catch (NameNotFoundException e) { +            Log.e(TAG, "Package not found. could not get version code."); +            return ""; +        } +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java new file mode 100644 index 0000000000..8b869ddaaf --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Callback for the license checker library. + * <p> + * Upon checking with the Market server and conferring with the {@link Policy}, + * the library calls the appropriate callback method to communicate the result. + * <p> + * <b>The callback does not occur in the original checking thread.</b> Your + * application should post to the appropriate handling thread or lock + * accordingly. + * <p> + * The reason that is passed back with allow/dontAllow is the base status handed + * to the policy for allowed/disallowing the license. Policy.RETRY will call + * allow or dontAllow depending on other statistics associated with the policy, + * while in most cases Policy.NOT_LICENSED will call dontAllow and + * Policy.LICENSED will Allow. + */ +public interface LicenseCheckerCallback { + +    /** +     * Allow use. App should proceed as normal. +     * +     * @param reason Policy.LICENSED or Policy.RETRY typically. (although in +     *            theory the policy can return Policy.NOT_LICENSED here as well) +     */ +    public void allow(int reason); + +    /** +     * Don't allow use. App should inform user and take appropriate action. +     * +     * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory +     *            the policy can return Policy.LICENSED here as well --- +     *            perhaps the call to the LVL took too long, for example) +     */ +    public void dontAllow(int reason); + +    /** Application error codes. */ +    public static final int ERROR_INVALID_PACKAGE_NAME = 1; +    public static final int ERROR_NON_MATCHING_UID = 2; +    public static final int ERROR_NOT_MARKET_MANAGED = 3; +    public static final int ERROR_CHECK_IN_PROGRESS = 4; +    public static final int ERROR_INVALID_PUBLIC_KEY = 5; +    public static final int ERROR_MISSING_PERMISSION = 6; + +    /** +     * Error in application code. Caller did not call or set up license checker +     * correctly. Should be considered fatal. +     */ +    public void applicationError(int errorCode); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java new file mode 100644 index 0000000000..11a00786d0 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/LicenseValidator.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import com.google.android.vending.licensing.util.Base64; +import com.google.android.vending.licensing.util.Base64DecoderException; + +import android.text.TextUtils; +import android.util.Log; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * Contains data related to a licensing request and methods to verify + * and process the response. + */ +class LicenseValidator { +    private static final String TAG = "LicenseValidator"; + +    // Server response codes. +    private static final int LICENSED = 0x0; +    private static final int NOT_LICENSED = 0x1; +    private static final int LICENSED_OLD_KEY = 0x2; +    private static final int ERROR_NOT_MARKET_MANAGED = 0x3; +    private static final int ERROR_SERVER_FAILURE = 0x4; +    private static final int ERROR_OVER_QUOTA = 0x5; + +    private static final int ERROR_CONTACTING_SERVER = 0x101; +    private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; +    private static final int ERROR_NON_MATCHING_UID = 0x103; + +    private final Policy mPolicy; +    private final LicenseCheckerCallback mCallback; +    private final int mNonce; +    private final String mPackageName; +    private final String mVersionCode; +    private final DeviceLimiter mDeviceLimiter; + +    LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, +             int nonce, String packageName, String versionCode) { +        mPolicy = policy; +        mDeviceLimiter = deviceLimiter; +        mCallback = callback; +        mNonce = nonce; +        mPackageName = packageName; +        mVersionCode = versionCode; +    } + +    public LicenseCheckerCallback getCallback() { +        return mCallback; +    } + +    public int getNonce() { +        return mNonce; +    } + +    public String getPackageName() { +        return mPackageName; +    } + +    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + +    /** +     * Verifies the response from server and calls appropriate callback method. +     * +     * @param publicKey public key associated with the developer account +     * @param responseCode server response code +     * @param signedData signed data from server +     * @param signature server signature +     */ +    public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { +        String userId = null; +        // Skip signature check for unsuccessful requests +        ResponseData data = null; +        if (responseCode == LICENSED || responseCode == NOT_LICENSED || +                responseCode == LICENSED_OLD_KEY) { +            // Verify signature. +            try { +                if (TextUtils.isEmpty(signedData)) { +                    Log.e(TAG, "Signature verification failed: signedData is empty. " + +                            "(Device not signed-in to any Google accounts?)"); +                    handleInvalidResponse(); +                    return; +                } + +                Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); +                sig.initVerify(publicKey); +                sig.update(signedData.getBytes()); + +                if (!sig.verify(Base64.decode(signature))) { +                    Log.e(TAG, "Signature verification failed."); +                    handleInvalidResponse(); +                    return; +                } +            } catch (NoSuchAlgorithmException e) { +                // This can't happen on an Android compatible device. +                throw new RuntimeException(e); +            } catch (InvalidKeyException e) { +                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); +                return; +            } catch (SignatureException e) { +                throw new RuntimeException(e); +            } catch (Base64DecoderException e) { +                Log.e(TAG, "Could not Base64-decode signature."); +                handleInvalidResponse(); +                return; +            } + +            // Parse and validate response. +            try { +                data = ResponseData.parse(signedData); +            } catch (IllegalArgumentException e) { +                Log.e(TAG, "Could not parse response."); +                handleInvalidResponse(); +                return; +            } + +            if (data.responseCode != responseCode) { +                Log.e(TAG, "Response codes don't match."); +                handleInvalidResponse(); +                return; +            } + +            if (data.nonce != mNonce) { +                Log.e(TAG, "Nonce doesn't match."); +                handleInvalidResponse(); +                return; +            } + +            if (!data.packageName.equals(mPackageName)) { +                Log.e(TAG, "Package name doesn't match."); +                handleInvalidResponse(); +                return; +            } + +            if (!data.versionCode.equals(mVersionCode)) { +                Log.e(TAG, "Version codes don't match."); +                handleInvalidResponse(); +                return; +            } + +            // Application-specific user identifier. +            userId = data.userId; +            if (TextUtils.isEmpty(userId)) { +                Log.e(TAG, "User identifier is empty."); +                handleInvalidResponse(); +                return; +            } +        } + +        switch (responseCode) { +            case LICENSED: +            case LICENSED_OLD_KEY: +                int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); +                handleResponse(limiterResponse, data); +                break; +            case NOT_LICENSED: +                handleResponse(Policy.NOT_LICENSED, data); +                break; +            case ERROR_CONTACTING_SERVER: +                Log.w(TAG, "Error contacting licensing server."); +                handleResponse(Policy.RETRY, data); +                break; +            case ERROR_SERVER_FAILURE: +                Log.w(TAG, "An error has occurred on the licensing server."); +                handleResponse(Policy.RETRY, data); +                break; +            case ERROR_OVER_QUOTA: +                Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); +                handleResponse(Policy.RETRY, data); +                break; +            case ERROR_INVALID_PACKAGE_NAME: +                handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); +                break; +            case ERROR_NON_MATCHING_UID: +                handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); +                break; +            case ERROR_NOT_MARKET_MANAGED: +                handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); +                break; +            default: +                Log.e(TAG, "Unknown response code for license check."); +                handleInvalidResponse(); +        } +    } + +    /** +     * Confers with policy and calls appropriate callback method. +     * +     * @param response +     * @param rawData +     */ +    private void handleResponse(int response, ResponseData rawData) { +        // Update policy data and increment retry counter (if needed) +        mPolicy.processServerResponse(response, rawData); + +        // Given everything we know, including cached data, ask the policy if we should grant +        // access. +        if (mPolicy.allowAccess()) { +            mCallback.allow(response); +        } else { +            mCallback.dontAllow(response); +        } +    } + +    private void handleApplicationError(int code) { +        mCallback.applicationError(code); +    } + +    private void handleInvalidResponse() { +        mCallback.dontAllow(Policy.NOT_LICENSED); +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java new file mode 100644 index 0000000000..d87af3153f --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * A DeviceLimiter that doesn't limit the number of devices that can use a + * given user's license. + * <p> + * Unless you have reason to believe that your application is being pirated + * by multiple users using the same license (signing in to Market as the same + * user), we recommend you use this implementation. + */ +public class NullDeviceLimiter implements DeviceLimiter { + +    public int isDeviceAllowed(String userId) { +        return Policy.LICENSED; +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java new file mode 100644 index 0000000000..008c150a8e --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Obfuscator.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Interface used as part of a {@link Policy} to allow application authors to obfuscate + * licensing data that will be stored into a SharedPreferences file. + * <p> + * Any transformation scheme must be reversable. Implementing classes may optionally implement an + * integrity check to further prevent modification to preference data. Implementing classes + * should use device-specific information as a key in the obfuscation algorithm to prevent + * obfuscated preferences from being shared among devices. + */ +public interface Obfuscator { + +    /** +     * Obfuscate a string that is being stored into shared preferences. +     * +     * @param original The data that is to be obfuscated. +     * @param key The key for the data that is to be obfuscated. +     * @return A transformed version of the original data. +     */ +    String obfuscate(String original, String key); + +    /** +     * Undo the transformation applied to data by the obfuscate() method. +     * +     * @param obfuscated The data that is to be un-obfuscated. +     * @param key The key for the data that is to be un-obfuscated. +     * @return The original data transformed by the obfuscate() method. +     * @throws ValidationException Optionally thrown if a data integrity check fails. +     */ +    String unobfuscate(String obfuscated, String key) throws ValidationException; +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java new file mode 100644 index 0000000000..b672a078b7 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/Policy.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Policy used by {@link LicenseChecker} to determine whether a user should have + * access to the application. + */ +public interface Policy { + +    /** +     * Change these values to make it more difficult for tools to automatically +     * strip LVL protection from your APK. +     */ + +    /** +     * LICENSED means that the server returned back a valid license response +     */ +    public static final int LICENSED = 0x0100; +    /** +     * NOT_LICENSED means that the server returned back a valid license response +     * that indicated that the user definitively is not licensed +     */ +    public static final int NOT_LICENSED = 0x0231; +    /** +     * RETRY means that the license response was unable to be determined --- +     * perhaps as a result of faulty networking +     */ +    public static final int RETRY = 0x0123; + +    /** +     * Provide results from contact with the license server. Retry counts are +     * incremented if the current value of response is RETRY. Results will be +     * used for any future policy decisions. +     * +     * @param response the result from validating the server response +     * @param rawData the raw server response data, can be null for RETRY +     */ +    void processServerResponse(int response, ResponseData rawData); + +    /** +     * Check if the user should be allowed access to the application. +     */ +    boolean allowAccess(); + +    /** +     * Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g. +     * buy app on the Play Store). +     */ +    String getLicensingUrl(); +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java b/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java new file mode 100644 index 0000000000..feb579af04 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/PreferenceObfuscator.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.content.SharedPreferences; +import android.util.Log; + +/** + * An wrapper for SharedPreferences that transparently performs data obfuscation. + */ +public class PreferenceObfuscator { + +    private static final String TAG = "PreferenceObfuscator"; + +    private final SharedPreferences mPreferences; +    private final Obfuscator mObfuscator; +    private SharedPreferences.Editor mEditor; + +    /** +     * Constructor. +     * +     * @param sp A SharedPreferences instance provided by the system. +     * @param o The Obfuscator to use when reading or writing data. +     */ +    public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { +        mPreferences = sp; +        mObfuscator = o; +        mEditor = null; +    } + +    public void putString(String key, String value) { +        if (mEditor == null) { +            mEditor = mPreferences.edit(); +            // -- GODOT start -- +            mEditor.apply(); +            // -- GODOT end -- +        } +        String obfuscatedValue = mObfuscator.obfuscate(value, key); +        mEditor.putString(key, obfuscatedValue); +    } + +    public String getString(String key, String defValue) { +        String result; +        String value = mPreferences.getString(key, null); +        if (value != null) { +            try { +                result = mObfuscator.unobfuscate(value, key); +            } catch (ValidationException e) { +                // Unable to unobfuscate, data corrupt or tampered +                Log.w(TAG, "Validation error while reading preference: " + key); +                result = defValue; +            } +        } else { +            // Preference not found +            result = defValue; +        } +        return result; +    } + +    public void commit() { +        if (mEditor != null) { +            mEditor.commit(); +            mEditor = null; +        } +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java new file mode 100644 index 0000000000..3b5d557e76 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ResponseData.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.text.TextUtils; + +import java.util.regex.Pattern; + +/** + * ResponseData from licensing server. + */ +public class ResponseData { + +    public int responseCode; +    public int nonce; +    public String packageName; +    public String versionCode; +    public String userId; +    public long timestamp; +    /** Response-specific data. */ +    public String extra; + +    /** +     * Parses response string into ResponseData. +     * +     * @param responseData response data string +     * @throws IllegalArgumentException upon parsing error +     * @return ResponseData object +     */ +    public static ResponseData parse(String responseData) { +        // Must parse out main response data and response-specific data. +        int index = responseData.indexOf(':'); +        String mainData, extraData; +        if (-1 == index) { +            mainData = responseData; +            extraData = ""; +        } else { +            mainData = responseData.substring(0, index); +            extraData = index >= responseData.length() ? "" : responseData.substring(index + 1); +        } + +        String[] fields = TextUtils.split(mainData, Pattern.quote("|")); +        if (fields.length < 6) { +            throw new IllegalArgumentException("Wrong number of fields."); +        } + +        ResponseData data = new ResponseData(); +        data.extra = extraData; +        data.responseCode = Integer.parseInt(fields[0]); +        data.nonce = Integer.parseInt(fields[1]); +        data.packageName = fields[2]; +        data.versionCode = fields[3]; +        // Application-specific user identifier. +        data.userId = fields[4]; +        data.timestamp = Long.parseLong(fields[5]); + +        return data; +    } + +    @Override +    public String toString() { +        return TextUtils.join("|", new Object[] { +                responseCode, nonce, packageName, versionCode, +                userId, timestamp +        }); +    } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000000..e2f0bfdca8 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.vending.licensing.util.URIQueryDecoder; + +/** + * Default policy. All policy decisions are based off of response data received + * from the licensing service. Specifically, the licensing server sends the + * following information: response validity period, error retry period, + * error retry count and a URL for restoring app access in unlicensed cases. + * <p> + * These values will vary based on the the way the application is configured in + * the Google Play publishing console, such as whether the application is + * marked as free or is within its refund period, as well as how often an + * application is checking with the licensing service. + * <p> + * Developers who need more fine grained control over their application's + * licensing policy should implement a custom Policy. + */ +public class ServerManagedPolicy implements Policy { + +    private static final String TAG = "ServerManagedPolicy"; +    private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy"; +    private static final String PREF_LAST_RESPONSE = "lastResponse"; +    private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; +    private static final String PREF_RETRY_UNTIL = "retryUntil"; +    private static final String PREF_MAX_RETRIES = "maxRetries"; +    private static final String PREF_RETRY_COUNT = "retryCount"; +    private static final String PREF_LICENSING_URL = "licensingUrl"; +    private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; +    private static final String DEFAULT_RETRY_UNTIL = "0"; +    private static final String DEFAULT_MAX_RETRIES = "0"; +    private static final String DEFAULT_RETRY_COUNT = "0"; + +    private static final long MILLIS_PER_MINUTE = 60 * 1000; + +    private long mValidityTimestamp; +    private long mRetryUntil; +    private long mMaxRetries; +    private long mRetryCount; +    private long mLastResponseTime = 0; +    private int mLastResponse; +    private String mLicensingUrl; +    private PreferenceObfuscator mPreferences; + +    /** +     * @param context The context for the current application +     * @param obfuscator An obfuscator to be used with preferences. +     */ +    public ServerManagedPolicy(Context context, Obfuscator obfuscator) { +        // Import old values +        SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); +        mPreferences = new PreferenceObfuscator(sp, obfuscator); +        mLastResponse = Integer.parseInt( +            mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); +        mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, +                DEFAULT_VALIDITY_TIMESTAMP)); +        mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); +        mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); +        mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); +        mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); +    } + +    /** +     * Process a new response from the license server. +     * <p> +     * This data will be used for computing future policy decisions. The +     * following parameters are processed: +     * <ul> +     * <li>VT: the timestamp that the client should consider the response valid +     * until +     * <li>GT: the timestamp that the client should ignore retry errors until +     * <li>GR: the number of retry errors that the client should ignore +     * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. +     * buy app on the Play Store) +     * </ul> +     * +     * @param response the result from validating the server response +     * @param rawData the raw server response data +     */ +    public void processServerResponse(int response, ResponseData rawData) { + +        // Update retry counter +        if (response != Policy.RETRY) { +            setRetryCount(0); +        } else { +            setRetryCount(mRetryCount + 1); +        } + +        // Update server policy data +        Map<String, String> extras = decodeExtras(rawData); +        if (response == Policy.LICENSED) { +            mLastResponse = response; +            // Reset the licensing URL since it is only applicable for NOT_LICENSED responses. +            setLicensingUrl(null); +            setValidityTimestamp(extras.get("VT")); +            setRetryUntil(extras.get("GT")); +            setMaxRetries(extras.get("GR")); +        } else if (response == Policy.NOT_LICENSED) { +            // Clear out stale retry params +            setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); +            setRetryUntil(DEFAULT_RETRY_UNTIL); +            setMaxRetries(DEFAULT_MAX_RETRIES); +            // Update the licensing URL +            setLicensingUrl(extras.get("LU")); +        } + +        setLastResponse(response); +        mPreferences.commit(); +    } + +    /** +     * Set the last license response received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param l the response +     */ +    private void setLastResponse(int l) { +        mLastResponseTime = System.currentTimeMillis(); +        mLastResponse = l; +        mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); +    } + +    /** +     * Set the current retry count and add to preferences. You must manually +     * call PreferenceObfuscator.commit() to commit these changes to disk. +     * +     * @param c the new retry count +     */ +    private void setRetryCount(long c) { +        mRetryCount = c; +        mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); +    } + +    public long getRetryCount() { +        return mRetryCount; +    } + +    /** +     * Set the last validity timestamp (VT) received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param validityTimestamp the VT string received +     */ +    private void setValidityTimestamp(String validityTimestamp) { +        Long lValidityTimestamp; +        try { +            lValidityTimestamp = Long.parseLong(validityTimestamp); +        } catch (NumberFormatException e) { +            // No response or not parsable, expire in one minute. +            Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); +            lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; +            validityTimestamp = Long.toString(lValidityTimestamp); +        } + +        mValidityTimestamp = lValidityTimestamp; +        mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); +    } + +    public long getValidityTimestamp() { +        return mValidityTimestamp; +    } + +    /** +     * Set the retry until timestamp (GT) received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param retryUntil the GT string received +     */ +    private void setRetryUntil(String retryUntil) { +        Long lRetryUntil; +        try { +            lRetryUntil = Long.parseLong(retryUntil); +        } catch (NumberFormatException e) { +            // No response or not parsable, expire immediately +            Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); +            retryUntil = "0"; +            lRetryUntil = 0l; +        } + +        mRetryUntil = lRetryUntil; +        mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); +    } + +    public long getRetryUntil() { +      return mRetryUntil; +    } + +    /** +     * Set the max retries value (GR) as received from the server and add to +     * preferences. You must manually call PreferenceObfuscator.commit() to +     * commit these changes to disk. +     * +     * @param maxRetries the GR string received +     */ +    private void setMaxRetries(String maxRetries) { +        Long lMaxRetries; +        try { +            lMaxRetries = Long.parseLong(maxRetries); +        } catch (NumberFormatException e) { +            // No response or not parsable, expire immediately +            Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); +            maxRetries = "0"; +            lMaxRetries = 0l; +        } + +        mMaxRetries = lMaxRetries; +        mPreferences.putString(PREF_MAX_RETRIES, maxRetries); +    } + +    public long getMaxRetries() { +        return mMaxRetries; +    } + +    /** +     * Set the license URL value (LU) as received from the server and add to preferences. You must +     * manually call PreferenceObfuscator.commit() to commit these changes to disk. +     * +     * @param url the LU string received +     */ +    private void setLicensingUrl(String url) { +        mLicensingUrl = url; +        mPreferences.putString(PREF_LICENSING_URL, url); +    } + +    public String getLicensingUrl() { +        return mLicensingUrl; +    } + +    /** +     * {@inheritDoc} +     * +     * This implementation allows access if either:<br> +     * <ol> +     * <li>a LICENSED response was received within the validity period +     * <li>a RETRY response was received in the last minute, and we are under +     * the RETRY count or in the RETRY period. +     * </ol> +     */ +    public boolean allowAccess() { +        long ts = System.currentTimeMillis(); +        if (mLastResponse == Policy.LICENSED) { +            // Check if the LICENSED response occurred within the validity timeout. +            if (ts <= mValidityTimestamp) { +                // Cached LICENSED response is still valid. +                return true; +            } +        } else if (mLastResponse == Policy.RETRY && +                   ts < mLastResponseTime + MILLIS_PER_MINUTE) { +            // Only allow access if we are within the retry period or we haven't used up our +            // max retries. +            return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); +        } +        return false; +    } + +    private Map<String, String> decodeExtras( +        com.google.android.vending.licensing.ResponseData rawData) { +        Map<String, String> results = new HashMap<String, String>(); +        if (rawData == null) { +            return results; +        } + +        try { +            URI rawExtras = new URI("?" + rawData.extra); +            URIQueryDecoder.DecodeQuery(rawExtras, results); +        } catch (URISyntaxException e) { +            Log.w(TAG, "Invalid syntax error while decoding extras data from server."); +        } +        return results; +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java new file mode 100644 index 0000000000..c2d55c37f1 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/StrictPolicy.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +import android.util.Log; +import com.google.android.vending.licensing.util.URIQueryDecoder; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** + * Non-caching policy. All requests will be sent to the licensing service, + * and no local caching is performed. + * <p> + * Using a non-caching policy ensures that there is no local preference data + * for malicious users to tamper with. As a side effect, applications + * will not be permitted to run while offline. Developers should carefully + * weigh the risks of using this Policy over one which implements caching, + * such as ServerManagedPolicy. + * <p> + * Access to the application is only allowed if a LICENSED response is. + * received. All other responses (including RETRY) will deny access. + */ +public class StrictPolicy implements Policy { + +    private static final String TAG = "StrictPolicy"; + +    private int mLastResponse; +    private String mLicensingUrl; + +    public StrictPolicy() { +        // Set default policy. This will force the application to check the policy on launch. +        mLastResponse = Policy.RETRY; +        mLicensingUrl = null; +    } + +    /** +     * Process a new response from the license server. Since we aren't +     * performing any caching, this equates to reading the LicenseResponse. +     * Any cache-related ResponseData is ignored, but the licensing URL +     * extra is still extracted in cases where the app is unlicensed. +     * +     * @param response the result from validating the server response +     * @param rawData the raw server response data +     */ +    public void processServerResponse(int response, ResponseData rawData) { +        mLastResponse = response; + +        if (response == Policy.NOT_LICENSED) { +            Map<String, String> extras = decodeExtras(rawData); +            mLicensingUrl = extras.get("LU"); +        } +    } + +    /** +     * {@inheritDoc} +     * +     * This implementation allows access if and only if a LICENSED response +     * was received the last time the server was contacted. +     */ +    public boolean allowAccess() { +        return (mLastResponse == Policy.LICENSED); +    } + +    public String getLicensingUrl() { +        return mLicensingUrl; +    } + +    private Map<String, String> decodeExtras( +        com.google.android.vending.licensing.ResponseData rawData) { +        Map<String, String> results = new HashMap<String, String>(); +        if (rawData == null) { +            return results; +        } + +        try { +            URI rawExtras = new URI("?" + rawData.extra); +            URIQueryDecoder.DecodeQuery(rawExtras, results); +        } catch (URISyntaxException e) { +            Log.w(TAG, "Invalid syntax error while decoding extras data from server."); +        } +        return results; +    } + +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java new file mode 100644 index 0000000000..ee4df47c68 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/ValidationException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing; + +/** + * Indicates that an error occurred while validating the integrity of data managed by an + * {@link Obfuscator}.} + */ +public class ValidationException extends Exception { +    public ValidationException() { +      super(); +    } + +    public ValidationException(String s) { +      super(s); +    } + +    private static final long serialVersionUID = 1L; +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java new file mode 100644 index 0000000000..79efca9621 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64.java @@ -0,0 +1,578 @@ +// Portions copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.vending.licensing.util; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + * <p> + * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> + * periodically to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +// -- GODOT start -- +import org.godotengine.godot.BuildConfig; +// -- GODOT end -- + +/** + * Base64 converter class. This code is not a full-blown MIME encoder; + * it simply converts binary data to base64 data and back. + * + * <p>Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { +  /** Specify encoding (value is {@code true}). */ +  public final static boolean ENCODE = true; + +  /** Specify decoding (value is {@code false}). */ +  public final static boolean DECODE = false; + +  /** The equals sign (=) as a byte. */ +  private final static byte EQUALS_SIGN = (byte) '='; + +  /** The new line character (\n) as a byte. */ +  private final static byte NEW_LINE = (byte) '\n'; + +  /** +   * The 64 valid Base64 values. +   */ +  private final static byte[] ALPHABET = +      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', +          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', +          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', +          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', +          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', +          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', +          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', +          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', +          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', +          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', +          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', +          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', +          (byte) '9', (byte) '+', (byte) '/'}; + +  /** +   * The 64 valid web safe Base64 values. +   */ +  private final static byte[] WEBSAFE_ALPHABET = +      {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', +          (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', +          (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', +          (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', +          (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', +          (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', +          (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', +          (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', +          (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', +          (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', +          (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', +          (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', +          (byte) '9', (byte) '-', (byte) '_'}; + +  /** +   * Translates a Base64 value to either its 6-bit reconstruction value +   * or a negative number indicating some other meaning. +   **/ +  private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 +      -5, -5, // Whitespace: Tab and Linefeed +      -9, -9, // Decimal 11 - 12 +      -5, // Whitespace: Carriage Return +      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 +      -9, -9, -9, -9, -9, // Decimal 27 - 31 +      -5, // Whitespace: Space +      -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 +      62, // Plus sign at decimal 43 +      -9, -9, -9, // Decimal 44 - 46 +      63, // Slash at decimal 47 +      52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine +      -9, -9, -9, // Decimal 58 - 60 +      -1, // Equals sign at decimal 61 +      -9, -9, -9, // Decimal 62 - 64 +      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' +      14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' +      -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 +      26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' +      39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' +      -9, -9, -9, -9, -9 // Decimal 123 - 127 +      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ +      }; + +  /** The web safe decodabet */ +  private final static byte[] WEBSAFE_DECODABET = +      {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 +          -5, -5, // Whitespace: Tab and Linefeed +          -9, -9, // Decimal 11 - 12 +          -5, // Whitespace: Carriage Return +          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 +          -9, -9, -9, -9, -9, // Decimal 27 - 31 +          -5, // Whitespace: Space +          -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 +          62, // Dash '-' sign at decimal 45 +          -9, -9, // Decimal 46-47 +          52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine +          -9, -9, -9, // Decimal 58 - 60 +          -1, // Equals sign at decimal 61 +          -9, -9, -9, // Decimal 62 - 64 +          0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' +          14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' +          -9, -9, -9, -9, // Decimal 91-94 +          63, // Underscore '_' at decimal 95 +          -9, // Decimal 96 +          26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' +          39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' +          -9, -9, -9, -9, -9 // Decimal 123 - 127 +      /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 +        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ +      }; + +  // Indicates white space in encoding +  private final static byte WHITE_SPACE_ENC = -5; +  // Indicates equals sign in encoding +  private final static byte EQUALS_SIGN_ENC = -1; + +  /** Defeats instantiation. */ +  private Base64() { +  } + +  /* ********  E N C O D I N G   M E T H O D S  ******** */ + +  /** +   * Encodes up to three bytes of the array <var>source</var> +   * and writes the resulting four Base64 bytes to <var>destination</var>. +   * The source and destination arrays can be manipulated +   * anywhere along their length by specifying +   * <var>srcOffset</var> and <var>destOffset</var>. +   * This method does not check to make sure your arrays +   * are large enough to accommodate <var>srcOffset</var> + 3 for +   * the <var>source</var> array or <var>destOffset</var> + 4 for +   * the <var>destination</var> array. +   * The actual number of significant bytes in your array is +   * given by <var>numSigBytes</var>. +   * +   * @param source the array to convert +   * @param srcOffset the index where conversion begins +   * @param numSigBytes the number of significant bytes in your array +   * @param destination the array to hold the conversion +   * @param destOffset the index where output will be put +   * @param alphabet is the encoding alphabet +   * @return the <var>destination</var> array +   * @since 1.3 +   */ +  private static byte[] encode3to4(byte[] source, int srcOffset, +      int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { +    //           1         2         3 +    // 01234567890123456789012345678901 Bit position +    // --------000000001111111122222222 Array position from threeBytes +    // --------|    ||    ||    ||    | Six bit groups to index alphabet +    //          >>18  >>12  >> 6  >> 0  Right shift necessary +    //                0x3f  0x3f  0x3f  Additional AND + +    // Create buffer with zero-padding if there are only one or two +    // significant bytes passed in the array. +    // We have to shift left 24 in order to flush out the 1's that appear +    // when Java treats a value as negative that is cast from a byte to an int. +    int inBuff = +        (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) +            | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) +            | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + +    switch (numSigBytes) { +      case 3: +        destination[destOffset] = alphabet[(inBuff >>> 18)]; +        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +        destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; +        return destination; +      case 2: +        destination[destOffset] = alphabet[(inBuff >>> 18)]; +        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +        destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +        destination[destOffset + 3] = EQUALS_SIGN; +        return destination; +      case 1: +        destination[destOffset] = alphabet[(inBuff >>> 18)]; +        destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +        destination[destOffset + 2] = EQUALS_SIGN; +        destination[destOffset + 3] = EQUALS_SIGN; +        return destination; +      default: +        return destination; +    } // end switch +  } // end encode3to4 + +  /** +   * Encodes a byte array into Base64 notation. +   * Equivalent to calling +   * {@code encodeBytes(source, 0, source.length)} +   * +   * @param source The data to convert +   * @since 1.4 +   */ +  public static String encode(byte[] source) { +    return encode(source, 0, source.length, ALPHABET, true); +  } + +  /** +   * Encodes a byte array into web safe Base64 notation. +   * +   * @param source The data to convert +   * @param doPadding is {@code true} to pad result with '=' chars +   *        if it does not fall on 3 byte boundaries +   */ +  public static String encodeWebSafe(byte[] source, boolean doPadding) { +    return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); +  } + +  /** +   * Encodes a byte array into Base64 notation. +   * +   * @param source The data to convert +   * @param off Offset in array where conversion should begin +   * @param len Length of data to convert +   * @param alphabet is the encoding alphabet +   * @param doPadding is {@code true} to pad result with '=' chars +   *        if it does not fall on 3 byte boundaries +   * @since 1.4 +   */ +  public static String encode(byte[] source, int off, int len, byte[] alphabet, +      boolean doPadding) { +    byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); +    int outLen = outBuff.length; + +    // If doPadding is false, set length to truncate '=' +    // padding characters +    while (doPadding == false && outLen > 0) { +      if (outBuff[outLen - 1] != '=') { +        break; +      } +      outLen -= 1; +    } + +    return new String(outBuff, 0, outLen); +  } + +  /** +   * Encodes a byte array into Base64 notation. +   * +   * @param source The data to convert +   * @param off Offset in array where conversion should begin +   * @param len Length of data to convert +   * @param alphabet is the encoding alphabet +   * @param maxLineLength maximum length of one line. +   * @return the BASE64-encoded byte array +   */ +  public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, +      int maxLineLength) { +    int lenDiv3 = (len + 2) / 3; // ceil(len / 3) +    int len43 = lenDiv3 * 4; +    byte[] outBuff = new byte[len43 // Main 4:3 +        + (len43 / maxLineLength)]; // New lines + +    int d = 0; +    int e = 0; +    int len2 = len - 2; +    int lineLength = 0; +    for (; d < len2; d += 3, e += 4) { + +      // The following block of code is the same as +      // encode3to4( source, d + off, 3, outBuff, e, alphabet ); +      // but inlined for faster encoding (~20% improvement) +      int inBuff = +          ((source[d + off] << 24) >>> 8) +              | ((source[d + 1 + off] << 24) >>> 16) +              | ((source[d + 2 + off] << 24) >>> 24); +      outBuff[e] = alphabet[(inBuff >>> 18)]; +      outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; +      outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; +      outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + +      lineLength += 4; +      if (lineLength == maxLineLength) { +        outBuff[e + 4] = NEW_LINE; +        e++; +        lineLength = 0; +      } // end if: end of line +    } // end for: each piece of array + +    if (d < len) { +      encode3to4(source, d + off, len - d, outBuff, e, alphabet); + +      lineLength += 4; +      if (lineLength == maxLineLength) { +        // Add a last newline +        outBuff[e + 4] = NEW_LINE; +        e++; +      } +      e += 4; +    } + +    // -- GODOT start -- +    //assert (e == outBuff.length); +    if (BuildConfig.DEBUG && e != outBuff.length) +      throw new RuntimeException(); +    // -- GODOT end -- +    return outBuff; +  } + + +  /* ********  D E C O D I N G   M E T H O D S  ******** */ + + +  /** +   * Decodes four bytes from array <var>source</var> +   * and writes the resulting bytes (up to three of them) +   * to <var>destination</var>. +   * The source and destination arrays can be manipulated +   * anywhere along their length by specifying +   * <var>srcOffset</var> and <var>destOffset</var>. +   * This method does not check to make sure your arrays +   * are large enough to accommodate <var>srcOffset</var> + 4 for +   * the <var>source</var> array or <var>destOffset</var> + 3 for +   * the <var>destination</var> array. +   * This method returns the actual number of bytes that +   * were converted from the Base64 encoding. +   * +   * +   * @param source the array to convert +   * @param srcOffset the index where conversion begins +   * @param destination the array to hold the conversion +   * @param destOffset the index where output will be put +   * @param decodabet the decodabet for decoding Base64 content +   * @return the number of decoded bytes converted +   * @since 1.3 +   */ +  private static int decode4to3(byte[] source, int srcOffset, +      byte[] destination, int destOffset, byte[] decodabet) { +    // Example: Dk== +    if (source[srcOffset + 2] == EQUALS_SIGN) { +      int outBuff = +          ((decodabet[source[srcOffset]] << 24) >>> 6) +              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + +      destination[destOffset] = (byte) (outBuff >>> 16); +      return 1; +    } else if (source[srcOffset + 3] == EQUALS_SIGN) { +      // Example: DkL= +      int outBuff = +          ((decodabet[source[srcOffset]] << 24) >>> 6) +              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) +              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + +      destination[destOffset] = (byte) (outBuff >>> 16); +      destination[destOffset + 1] = (byte) (outBuff >>> 8); +      return 2; +    } else { +      // Example: DkLE +      int outBuff = +          ((decodabet[source[srcOffset]] << 24) >>> 6) +              | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) +              | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) +              | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + +      destination[destOffset] = (byte) (outBuff >> 16); +      destination[destOffset + 1] = (byte) (outBuff >> 8); +      destination[destOffset + 2] = (byte) (outBuff); +      return 3; +    } +  } // end decodeToBytes + + +  /** +   * Decodes data from Base64 notation. +   * +   * @param s the string to decode (decoded in default encoding) +   * @return the decoded data +   * @since 1.4 +   */ +  public static byte[] decode(String s) throws Base64DecoderException { +    byte[] bytes = s.getBytes(); +    return decode(bytes, 0, bytes.length); +  } + +  /** +   * Decodes data from web safe Base64 notation. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param s the string to decode (decoded in default encoding) +   * @return the decoded data +   */ +  public static byte[] decodeWebSafe(String s) throws Base64DecoderException { +    byte[] bytes = s.getBytes(); +    return decodeWebSafe(bytes, 0, bytes.length); +  } + +  /** +   * Decodes Base64 content in byte array format and returns +   * the decoded byte array. +   * +   * @param source The Base64 encoded data +   * @return decoded data +   * @since 1.3 +   * @throws Base64DecoderException +   */ +  public static byte[] decode(byte[] source) throws Base64DecoderException { +    return decode(source, 0, source.length); +  } + +  /** +   * Decodes web safe Base64 content in byte array format and returns +   * the decoded data. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param source the string to decode (decoded in default encoding) +   * @return the decoded data +   */ +  public static byte[] decodeWebSafe(byte[] source) +      throws Base64DecoderException { +    return decodeWebSafe(source, 0, source.length); +  } + +  /** +   * Decodes Base64 content in byte array format and returns +   * the decoded byte array. +   * +   * @param source The Base64 encoded data +   * @param off    The offset of where to begin decoding +   * @param len    The length of characters to decode +   * @return decoded data +   * @since 1.3 +   * @throws Base64DecoderException +   */ +  public static byte[] decode(byte[] source, int off, int len) +      throws Base64DecoderException { +    return decode(source, off, len, DECODABET); +  } + +  /** +   * Decodes web safe Base64 content in byte array format and returns +   * the decoded byte array. +   * Web safe encoding uses '-' instead of '+', '_' instead of '/' +   * +   * @param source The Base64 encoded data +   * @param off    The offset of where to begin decoding +   * @param len    The length of characters to decode +   * @return decoded data +   */ +  public static byte[] decodeWebSafe(byte[] source, int off, int len) +      throws Base64DecoderException { +    return decode(source, off, len, WEBSAFE_DECODABET); +  } + +  /** +   * Decodes Base64 content using the supplied decodabet and returns +   * the decoded byte array. +   * +   * @param source    The Base64 encoded data +   * @param off       The offset of where to begin decoding +   * @param len       The length of characters to decode +   * @param decodabet the decodabet for decoding Base64 content +   * @return decoded data +   */ +  public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) +      throws Base64DecoderException { +    int len34 = len * 3 / 4; +    byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output +    int outBuffPosn = 0; + +    byte[] b4 = new byte[4]; +    int b4Posn = 0; +    int i = 0; +    byte sbiCrop = 0; +    byte sbiDecode = 0; +    for (i = 0; i < len; i++) { +      sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits +      sbiDecode = decodabet[sbiCrop]; + +      if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better +        if (sbiDecode >= EQUALS_SIGN_ENC) { +          // An equals sign (for padding) must not occur at position 0 or 1 +          // and must be the last byte[s] in the encoded value +          if (sbiCrop == EQUALS_SIGN) { +            int bytesLeft = len - i; +            byte lastByte = (byte) (source[len - 1 + off] & 0x7f); +            if (b4Posn == 0 || b4Posn == 1) { +              throw new Base64DecoderException( +                  "invalid padding byte '=' at byte offset " + i); +            } else if ((b4Posn == 3 && bytesLeft > 2) +                || (b4Posn == 4 && bytesLeft > 1)) { +              throw new Base64DecoderException( +                  "padding byte '=' falsely signals end of encoded value " +                      + "at offset " + i); +            } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { +              throw new Base64DecoderException( +                  "encoded value has invalid trailing byte"); +            } +            break; +          } + +          b4[b4Posn++] = sbiCrop; +          if (b4Posn == 4) { +            outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); +            b4Posn = 0; +          } +        } +      } else { +        throw new Base64DecoderException("Bad Base64 input character at " + i +            + ": " + source[i + off] + "(decimal)"); +      } +    } + +    // Because web safe encoding allows non padding base64 encodes, we +    // need to pad the rest of the b4 buffer with equal signs when +    // b4Posn != 0.  There can be at most 2 equal signs at the end of +    // four characters, so the b4 buffer must have two or three +    // characters.  This also catches the case where the input is +    // padded with EQUALS_SIGN +    if (b4Posn != 0) { +      if (b4Posn == 1) { +        throw new Base64DecoderException("single trailing character at offset " +            + (len - 1)); +      } +      b4[b4Posn++] = EQUALS_SIGN; +      outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); +    } + +    byte[] out = new byte[outBuffPosn]; +    System.arraycopy(outBuff, 0, out, 0, outBuffPosn); +    return out; +  } +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java new file mode 100644 index 0000000000..1aef1b54b8 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//     http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.vending.licensing.util; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { +  public Base64DecoderException() { +    super(); +  } + +  public Base64DecoderException(String s) { +    super(s); +  } + +  private static final long serialVersionUID = 1L; +} diff --git a/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000000..5155bf5ac3 --- /dev/null +++ b/platform/android/java/lib/src/com/google/android/vending/licensing/util/URIQueryDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing.util; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Map; +import java.util.Scanner; + +public class URIQueryDecoder { +    private static final String TAG = "URIQueryDecoder"; + +    /** +     * Decodes the query portion of the passed-in URI. +     * +     * @param encodedURI the URI containing the query to decode +     * @param results a map containing all query parameters. Query parameters that do not have a +     *            value will map to a null string +     */ +    static public void DecodeQuery(URI encodedURI, Map<String, String> results) { +        Scanner scanner = new Scanner(encodedURI.getRawQuery()); +        scanner.useDelimiter("&"); +        try { +            while (scanner.hasNext()) { +                String param = scanner.next(); +                String[] valuePair = param.split("="); +                String name, value; +                if (valuePair.length == 1) { +                    value = null; +                } else if (valuePair.length == 2) { +                    value = URLDecoder.decode(valuePair[1], "UTF-8"); +                } else { +                    throw new IllegalArgumentException("query parameter invalid"); +                } +                name = URLDecoder.decode(valuePair[0], "UTF-8"); +                results.put(name, value); +            } +        } catch (UnsupportedEncodingException e) { +            // This should never happen. +            Log.e(TAG, "UTF-8 Not Recognized as a charset.  Device configuration Error."); +        } +    } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java new file mode 100644 index 0000000000..588d9ae646 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Dictionary.java @@ -0,0 +1,81 @@ +/*************************************************************************/ +/*  Dictionary.java                                                      */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */ +/*                                                                       */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the       */ +/* "Software"), to deal in the Software without restriction, including   */ +/* without limitation the rights to use, copy, modify, merge, publish,   */ +/* distribute, sublicense, and/or sell copies of the Software, and to    */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions:                                             */ +/*                                                                       */ +/* The above copyright notice and this permission notice shall be        */ +/* included in all copies or substantial portions of the Software.       */ +/*                                                                       */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ +/*************************************************************************/ + +package org.godotengine.godot; + +import java.util.HashMap; +import java.util.Set; + +public class Dictionary extends HashMap<String, Object> { + +	protected String[] keys_cache; + +	public String[] get_keys() { + +		String[] ret = new String[size()]; +		int i = 0; +		Set<String> keys = keySet(); +		for (String key : keys) { + +			ret[i] = key; +			i++; +		}; + +		return ret; +	}; + +	public Object[] get_values() { + +		Object[] ret = new Object[size()]; +		int i = 0; +		Set<String> keys = keySet(); +		for (String key : keys) { + +			ret[i] = get(key); +			i++; +		}; + +		return ret; +	}; + +	public void set_keys(String[] keys) { +		keys_cache = keys; +	}; + +	public void set_values(Object[] vals) { + +		int i = 0; +		for (String key : keys_cache) { +			put(key, vals[i]); +			i++; +		}; +		keys_cache = null; +	}; +}; diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java new file mode 100644 index 0000000000..4dae2dcc53 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -0,0 +1,1113 @@ +/*************************************************************************/ +/*  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.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.Handler; +import android.os.Looper; +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.annotation.Nullable; +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.utils.PermissionsUtil; +import org.godotengine.godot.xr.XRMode; + +public abstract class Godot extends Activity implements SensorEventListener, IDownloaderClient { + +	static final int MAX_SINGLETONS = 64; +	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; + +	// Used to dispatch events to the main thread. +	private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + +	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; +		} + +		/** +		 * Invoked once during the Godot Android initialization process after creation of the +		 * {@link GodotView} view. +		 * <p> +		 * This method should be overridden by descendants of this class that would like to add +		 * their view/layout to the Godot view hierarchy. +		 * +		 * @return the view to be included; null if no views should be included. +		 */ +		@Nullable +		protected View onMainCreateView(Activity activity) { +			return null; +		} + +		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"))); + +				// The Godot Android plugins are setup on completion of GodotLib.setup +				mainThreadHandler.post(new Runnable() { +					@Override +					public void run() { +						// Include the non-null views returned in the Godot view hierarchy. +						for (int i = 0; i < singleton_count; i++) { +							View view = singletons[i].onMainCreateView(Godot.this); +							if (view != null) { +								layout.addView(view); +							} +						} +					} +				}); +			} +		}); +	} + +	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(); + +		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(); +		activityResumed = true; +		if (!godot_initialized) { +			if (null != mDownloaderClientStub) { +				mDownloaderClientStub.connect(this); +			} +			return; +		} + +		mView.onResume(); + +		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(); +		} +	} + +	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) { +		return PermissionsUtil.requestPermission(p_name, this); +	} + +	public boolean requestPermissions() { +		return PermissionsUtil.requestManifestPermissions(this); +	} + +	public String[] getGrantedPermissions() { +		return PermissionsUtil.getGrantedPermissions(this); +	} + +	/** +	 * 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..67dce172dc --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -0,0 +1,231 @@ +/*************************************************************************/ +/*  GodotLib.java                                                        */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */ +/*                                                                       */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the       */ +/* "Software"), to deal in the Software without restriction, including   */ +/* without limitation the rights to use, copy, modify, merge, publish,   */ +/* distribute, sublicense, and/or sell copies of the Software, and to    */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions:                                             */ +/*                                                                       */ +/* The above copyright notice and this permission notice shall be        */ +/* included in all copies or substantial portions of the Software.       */ +/*                                                                       */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ +/*************************************************************************/ + +package org.godotengine.godot; + +import android.app.Activity; +import android.hardware.SensorEvent; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Wrapper for native library + */ +public class GodotLib { + +	public static GodotIO io; + +	static { +		System.loadLibrary("godot_android"); +	} + +	/** +	 * Invoked on the main thread to initialize Godot native layer. +	 */ +	public static native void initialize(Godot p_instance, Object p_asset_manager, boolean use_apk_expansion); + +	/** +	 * Invoked on the main thread to clean up Godot native layer. +	 * @see Activity#onDestroy() +	 */ +	public static native void ondestroy(Godot p_instance); + +	/** +	 * Invoked on the GL thread to complete setup for the Godot native layer logic. +	 * @param p_cmdline Command line arguments used to configure Godot native layer components. +	 */ +	public static native void setup(String[] p_cmdline); + +	/** +	 * Invoked on the GL thread when the underlying Android surface has changed size. +	 * @param width +	 * @param height +	 * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) +	 */ +	public static native void resize(int width, int height); + +	/** +	 * Invoked on the GL thread when the underlying Android surface is created or recreated. +	 * @param p_32_bits +	 * @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig) +	 */ +	public static native void newcontext(boolean p_32_bits); + +	/** +	 * Forward {@link Activity#onBackPressed()} event from the main thread to the GL thread. +	 */ +	public static native void back(); + +	/** +	 * Invoked on the GL thread to draw the current frame. +	 * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10) +	 */ +	public static native void step(); + +	/** +	 * Forward touch events from the main thread to the GL thread. +	 */ +	public static native void touch(int what, int pointer, int howmany, int[] arr); + +	/** +	 * Forward hover events from the main thread to the GL thread. +	 */ +	public static native void hover(int type, int x, int y); + +	/** +	 * Forward accelerometer sensor events from the main thread to the GL thread. +	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) +	 */ +	public static native void accelerometer(float x, float y, float z); + +	/** +	 * Forward gravity sensor events from the main thread to the GL thread. +	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) +	 */ +	public static native void gravity(float x, float y, float z); + +	/** +	 * Forward magnetometer sensor events from the main thread to the GL thread. +	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) +	 */ +	public static native void magnetometer(float x, float y, float z); + +	/** +	 * Forward gyroscope sensor events from the main thread to the GL thread. +	 * @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent) +	 */ +	public static native void gyroscope(float x, float y, float z); + +	/** +	 * Forward regular key events from the main thread to the GL thread. +	 */ +	public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed); + +	/** +	 * Forward game device's key events from the main thread to the GL thread. +	 */ +	public static native void joybutton(int p_device, int p_but, boolean p_pressed); + +	/** +	 * Forward joystick devices axis motion events from the main thread to the GL thread. +	 */ +	public static native void joyaxis(int p_device, int p_axis, float p_value); + +	/** +	 * Forward joystick devices hat motion events from the main thread to the GL thread. +	 */ +	public static native void joyhat(int p_device, int p_hat_x, int p_hat_y); + +	/** +	 * Fires when a joystick device is added or removed. +	 */ +	public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name); + +	/** +	 * Invoked when the Android activity resumes. +	 * @see Activity#onResume() +	 */ +	public static native void focusin(); + +	/** +	 * Invoked when the Android activity pauses. +	 * @see Activity#onPause() +	 */ +	public static native void focusout(); + +	/** +	 * Invoked when the audio thread is started. +	 */ +	public static native void audio(); + +	/** +	 * Used to setup a {@link org.godotengine.godot.Godot.SingletonBase} instance. +	 * @param p_name Name of the instance. +	 * @param p_object Reference to the singleton instance. +	 */ +	public static native void singleton(String p_name, Object p_object); + +	/** +	 * Used to complete registration of the {@link org.godotengine.godot.Godot.SingletonBase} instance's methods. +	 * @param p_sname Name of the instance +	 * @param p_name Name of the method to register +	 * @param p_ret Return type of the registered method +	 * @param p_params Method parameters types +	 */ +	public static native void method(String p_sname, String p_name, String p_ret, String[] p_params); + +	/** +	 * Used to access Godot global properties. +	 * @param p_key Property key +	 * @return String value of the property +	 */ +	public static native String getGlobal(String p_key); + +	/** +	 * Invoke method |p_method| on the Godot object specified by |p_id| +	 * @param p_id Id of the Godot object to invoke +	 * @param p_method Name of the method to invoke +	 * @param p_params Parameters to use for method invocation +	 */ +	public static native void callobject(int p_id, String p_method, Object[] p_params); + +	/** +	 * Invoke method |p_method| on the Godot object specified by |p_id| during idle time. +	 * @param p_id Id of the Godot object to invoke +	 * @param p_method Name of the method to invoke +	 * @param p_params Parameters to use for method invocation +	 */ +	public static native void calldeferred(int p_id, String p_method, Object[] p_params); + +	/** +	 * Forward the results from a permission request. +	 * @see Activity#onRequestPermissionsResult(int, String[], int[]) +	 * @param p_permission Request permission +	 * @param p_result True if the permission was granted, false otherwise +	 */ +	public static native void requestPermissionResult(String p_permission, boolean p_result); + +	/** +	 * Invoked on the GL thread to configure the height of the virtual keyboard. +	 */ +	public static native void setVirtualKeyboardHeight(int p_height); + +	/** +	 * Invoked on the GL thread when the {@link GodotRenderer} has been resumed. +	 * @see GodotRenderer#onActivityResumed() +	 */ +	public static native void onRendererResumed(); + +	/** +	 * Invoked on the GL thread when the {@link GodotRenderer} has been paused. +	 * @see GodotRenderer#onActivityPaused() +	 */ +	public static native void onRendererPaused(); +} diff --git a/platform/android/java/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..56ba88656e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java @@ -0,0 +1,78 @@ +/*************************************************************************/ +/*  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 { + +	private boolean activityJustResumed = false; + +	public void onDrawFrame(GL10 gl) { +		if (activityJustResumed) { +			GodotLib.onRendererResumed(); +			activityJustResumed = false; +		} + +		GodotLib.step(); +		for (int i = 0; i < Godot.singleton_count; i++) { +			Godot.singletons[i].onGLDrawFrame(gl); +		} +	} + +	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); +	} + +	void onActivityResumed() { +		// We defer invoking GodotLib.onRendererResumed() until the first draw frame call. +		// This ensures we have a valid GL context and surface when we do so. +		activityJustResumed = true; +	} + +	void onActivityPaused() { +		GodotLib.onRendererPaused(); +	} +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java new file mode 100644 index 0000000000..5511e5d782 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotView.java @@ -0,0 +1,200 @@ +/*************************************************************************/ +/*  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; +	private final GodotRenderer godotRenderer; + +	public GodotView(Godot activity, XRMode xrMode, boolean p_use_gl3, boolean p_use_32_bits, boolean p_use_debug_opengl) { +		super(activity); +		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); +		this.godotRenderer = new GodotRenderer(); +		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(godotRenderer); +	} + +	public void onBackPressed() { +		activity.onBackPressed(); +	} + +	@Override +	public void onResume() { +		super.onResume(); + +		queueEvent(new Runnable() { +			@Override +			public void run() { +				// Resume the renderer +				godotRenderer.onActivityResumed(); +				GodotLib.focusin(); +			} +		}); +	} + +	@Override +	public void onPause() { +		super.onPause(); + +		queueEvent(new Runnable() { +			@Override +			public void run() { +				GodotLib.focusout(); +				// Pause the renderer +				godotRenderer.onActivityPaused(); +			} +		}); +	} +} diff --git a/platform/android/java/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..2756ca6c83 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -0,0 +1,369 @@ +/*************************************************************************/ +/*  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); +					} +				}); +			} +		} else { +			final int chr = event.getUnicodeChar(0); +			queueEvent(new Runnable() { +				@Override +				public void run() { +					GodotLib.key(keyCode, chr, false); +				} +			}); +		}; + +		return true; +	} + +	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); +					} +				}); +			} +		} else { +			final int chr = event.getUnicodeChar(0); +			queueEvent(new Runnable() { +				@Override +				public void run() { +					GodotLib.key(keyCode, chr, true); +				} +			}); +		}; + +		return true; +	} + +	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; +			} +		} else if ((event.getSource() & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS) { +			final int x = Math.round(event.getX()); +			final int y = Math.round(event.getY()); +			final int type = event.getAction(); +			queueEvent(new Runnable() { +				@Override +				public void run() { +					GodotLib.hover(type, x, y); +				} +			}); +			return true; +		} + +		return false; +	} + +	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/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java new file mode 100644 index 0000000000..2c4a444e5a --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -0,0 +1,157 @@ +package org.godotengine.godot.utils; + +import android.Manifest; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.os.Build; +import android.support.v4.content.ContextCompat; +import java.util.ArrayList; +import java.util.List; +import org.godotengine.godot.Godot; + +/** + * This class includes utility functions for Android permissions related operations. + * @author Cagdas Caglak <cagdascaglak@gmail.com> + */ +public final class PermissionsUtil { + +	static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; +	static final int REQUEST_CAMERA_PERMISSION = 2; +	static final int REQUEST_VIBRATE_PERMISSION = 3; +	static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + +	private PermissionsUtil() { +	} + +	/** +	 * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a> +	 * @param name the name of the requested permission. +	 * @param activity the caller activity for this method. +	 * @return true/false. "true" if permission was granted otherwise returns "false". +	 */ +	public static boolean requestPermission(String name, Godot activity) { +		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { +			// Not necessary, asked on install already +			return true; +		} + +		if (name.equals("RECORD_AUDIO") && ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { +			activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); +			return false; +		} + +		if (name.equals("CAMERA") && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { +			activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); +			return false; +		} + +		if (name.equals("VIBRATE") && ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { +			activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); +			return false; +		} +		return true; +	} + +	/** +	 * Request dangerous permissions which are defined in the Android manifest file from the user. +	 * @param activity the caller activity for this method. +	 * @return true/false. "true" if all permissions were granted otherwise returns "false". +	 */ +	public static boolean requestManifestPermissions(Godot activity) { +		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { +			return true; +		} + +		String[] manifestPermissions; +		try { +			manifestPermissions = getManifestPermissions(activity); +		} catch (PackageManager.NameNotFoundException e) { +			e.printStackTrace(); +			return false; +		} + +		if (manifestPermissions == null || manifestPermissions.length == 0) +			return true; + +		List<String> dangerousPermissions = new ArrayList<>(); +		for (String manifestPermission : manifestPermissions) { +			try { +				PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); +				int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; +				if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { +					dangerousPermissions.add(manifestPermission); +				} +			} catch (PackageManager.NameNotFoundException e) { +				e.printStackTrace(); +				return false; +			} +		} + +		if (dangerousPermissions.isEmpty()) { +			// If list is empty, all of dangerous permissions were granted. +			return true; +		} + +		String[] requestedPermissions = dangerousPermissions.toArray(new String[0]); +		activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE); +		return false; +	} + +	/** +	 * With this function you can get the list of dangerous permissions that have been granted to the Android application. +	 * @param activity the caller activity for this method. +	 * @return granted permissions list +	 */ +	public static String[] getGrantedPermissions(Godot activity) { +		String[] manifestPermissions; +		try { +			manifestPermissions = getManifestPermissions(activity); +		} catch (PackageManager.NameNotFoundException e) { +			e.printStackTrace(); +			return new String[0]; +		} +		if (manifestPermissions == null || manifestPermissions.length == 0) +			return new String[0]; + +		List<String> dangerousPermissions = new ArrayList<>(); +		for (String manifestPermission : manifestPermissions) { +			try { +				PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); +				int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; +				if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { +					dangerousPermissions.add(manifestPermission); +				} +			} catch (PackageManager.NameNotFoundException e) { +				e.printStackTrace(); +				return new String[0]; +			} +		} + +		return dangerousPermissions.toArray(new String[0]); +	} + +	/** +	 * Returns the permissions defined in the AndroidManifest.xml file. +	 * @param activity the caller activity for this method. +	 * @return manifest permissions list +	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. +	 */ +	private static String[] getManifestPermissions(Godot activity) throws PackageManager.NameNotFoundException { +		PackageManager packageManager = activity.getPackageManager(); +		PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS); +		return packageInfo.requestedPermissions; +	} + +	/** +	 * Returns the information of the desired permission. +	 * @param activity the caller activity for this method. +	 * @param permission the name of the permission. +	 * @return permission info object +	 * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. +	 */ +	private static PermissionInfo getPermissionInfo(Godot activity, String permission) throws PackageManager.NameNotFoundException { +		PackageManager packageManager = activity.getPackageManager(); +		return packageManager.getPermissionInfo(permission, 0); +	} +} 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; +	} +}  |