From bcac6a3b8509f8903f94f053bde801b0063e8a33 Mon Sep 17 00:00:00 2001 From: Philippe-Adrien Nousse Date: Sun, 25 Mar 2018 13:13:01 +0200 Subject: [PATCH] Base Configuration --- .gitignore | 9 + .gitmodules | 6 + .idea/dictionaries/Philippe_Adrien.xml | 3 + .idea/gradle.xml | 20 + .idea/misc.xml | 49 + .idea/modules.xml | 11 + .idea/runConfigurations.xml | 12 + ExternalLibs/Parse-SDK-Android/.codecov.yml | 11 + ExternalLibs/Parse-SDK-Android/.gitignore | 40 + ExternalLibs/Parse-SDK-Android/.travis.yml | 35 + .../Parse-SDK-Android/CONTRIBUTING.md | 65 + ExternalLibs/Parse-SDK-Android/LICENSE | 34 + ExternalLibs/Parse-SDK-Android/PATENTS | 37 + .../Parse-SDK-Android/Parse/build.gradle | 52 + .../Parse/release-proguard.pro | 7 + .../Parse/src/main/AndroidManifest.xml | 23 + .../com/parse/AbstractQueryController.java | 38 + .../com/parse/AuthenticationCallback.java | 31 + .../java/com/parse/CacheQueryController.java | 175 + .../CachedCurrentInstallationController.java | 162 + .../parse/CachedCurrentUserController.java | 290 ++ .../main/java/com/parse/ConfigCallback.java | 44 + .../java/com/parse/ConnectivityNotifier.java | 96 + .../main/java/com/parse/CountCallback.java | 48 + .../main/java/com/parse/DeleteCallback.java | 44 + .../main/java/com/parse/EventuallyPin.java | 191 + .../main/java/com/parse/FileObjectStore.java | 139 + .../src/main/java/com/parse/FindCallback.java | 49 + .../main/java/com/parse/FunctionCallback.java | 49 + .../java/com/parse/GcmBroadcastReceiver.java | 25 + .../main/java/com/parse/GcmPushHandler.java | 211 + .../src/main/java/com/parse/GcmRegistrar.java | 405 ++ .../src/main/java/com/parse/GetCallback.java | 47 + .../main/java/com/parse/GetDataCallback.java | 40 + .../java/com/parse/GetDataStreamCallback.java | 41 + .../main/java/com/parse/GetFileCallback.java | 41 + .../main/java/com/parse/InstallationId.java | 90 + .../com/parse/KnownParseObjectDecoder.java | 36 + .../Parse/src/main/java/com/parse/Lists.java | 76 + .../main/java/com/parse/LocalIdManager.java | 205 + .../main/java/com/parse/LocationCallback.java | 48 + .../main/java/com/parse/LocationNotifier.java | 118 + .../src/main/java/com/parse/LockSet.java | 58 + .../main/java/com/parse/LogInCallback.java | 47 + .../main/java/com/parse/LogOutCallback.java | 43 + .../src/main/java/com/parse/ManifestInfo.java | 354 ++ .../com/parse/NetworkObjectController.java | 151 + .../com/parse/NetworkQueryController.java | 148 + .../com/parse/NetworkSessionController.java | 63 + .../java/com/parse/NetworkUserController.java | 142 + .../main/java/com/parse/NoObjectsEncoder.java | 29 + .../java/com/parse/NotificationCompat.java | 440 ++ .../src/main/java/com/parse/Numbers.java | 79 + .../java/com/parse/OfflineObjectStore.java | 134 + .../com/parse/OfflineQueryController.java | 49 + .../java/com/parse/OfflineQueryLogic.java | 1119 +++++ .../com/parse/OfflineSQLiteOpenHelper.java | 109 + .../src/main/java/com/parse/OfflineStore.java | 1567 ++++++ .../Parse/src/main/java/com/parse/PLog.java | 95 + .../Parse/src/main/java/com/parse/Parse.java | 770 +++ .../src/main/java/com/parse/ParseACL.java | 611 +++ .../java/com/parse/ParseAddOperation.java | 95 + .../com/parse/ParseAddUniqueOperation.java | 116 + .../main/java/com/parse/ParseAnalytics.java | 243 + .../com/parse/ParseAnalyticsController.java | 41 + .../java/com/parse/ParseAnonymousUtils.java | 82 + .../com/parse/ParseAuthenticationManager.java | 91 + .../com/parse/ParseByteArrayHttpBody.java | 47 + .../main/java/com/parse/ParseCallback1.java | 29 + .../main/java/com/parse/ParseCallback2.java | 32 + .../main/java/com/parse/ParseClassName.java | 28 + .../src/main/java/com/parse/ParseCloud.java | 107 + .../com/parse/ParseCloudCodeController.java | 59 + .../java/com/parse/ParseCommandCache.java | 688 +++ .../src/main/java/com/parse/ParseConfig.java | 563 +++ .../java/com/parse/ParseConfigController.java | 47 + .../main/java/com/parse/ParseCorePlugins.java | 356 ++ .../parse/ParseCountingByteArrayHttpBody.java | 51 + .../com/parse/ParseCountingFileHttpBody.java | 58 + .../parse/ParseCurrentConfigController.java | 100 + .../ParseCurrentInstallationController.java | 13 + .../com/parse/ParseCurrentUserController.java | 41 + .../main/java/com/parse/ParseDateFormat.java | 56 + .../src/main/java/com/parse/ParseDecoder.java | 156 + .../com/parse/ParseDefaultACLController.java | 68 + .../java/com/parse/ParseDeleteOperation.java | 52 + .../main/java/com/parse/ParseDigestUtils.java | 49 + .../src/main/java/com/parse/ParseEncoder.java | 163 + .../java/com/parse/ParseEventuallyQueue.java | 230 + .../main/java/com/parse/ParseException.java | 291 ++ .../main/java/com/parse/ParseExecutors.java | 39 + .../java/com/parse/ParseFieldOperation.java | 83 + .../java/com/parse/ParseFieldOperations.java | 253 + .../src/main/java/com/parse/ParseFile.java | 774 +++ .../java/com/parse/ParseFileController.java | 238 + .../java/com/parse/ParseFileHttpBody.java | 50 + .../main/java/com/parse/ParseFileRequest.java | 82 + .../main/java/com/parse/ParseFileUtils.java | 546 ++ .../main/java/com/parse/ParseGeoPoint.java | 336 ++ .../main/java/com/parse/ParseHttpClient.java | 186 + .../src/main/java/com/parse/ParseIOUtils.java | 352 ++ .../com/parse/ParseImpreciseDateFormat.java | 56 + .../com/parse/ParseIncrementOperation.java | 73 + .../java/com/parse/ParseInstallation.java | 321 ++ .../main/java/com/parse/ParseJSONUtils.java | 70 + .../java/com/parse/ParseKeyValueCache.java | 243 + .../com/parse/ParseMulticastDelegate.java | 39 + .../com/parse/ParseNotificationManager.java | 75 + .../src/main/java/com/parse/ParseObject.java | 4386 +++++++++++++++++ .../main/java/com/parse/ParseObjectCoder.java | 135 + .../java/com/parse/ParseObjectController.java | 37 + .../com/parse/ParseObjectCurrentCoder.java | 184 + .../parse/ParseObjectCurrentController.java | 52 + .../com/parse/ParseObjectParcelDecoder.java | 43 + .../com/parse/ParseObjectParcelEncoder.java | 38 + .../main/java/com/parse/ParseObjectStore.java | 22 + .../ParseObjectSubclassingController.java | 130 + .../java/com/parse/ParseOperationSet.java | 169 + .../java/com/parse/ParseParcelDecoder.java | 117 + .../java/com/parse/ParseParcelEncoder.java | 150 + .../src/main/java/com/parse/ParsePin.java | 43 + .../parse/ParsePinningEventuallyQueue.java | 638 +++ .../src/main/java/com/parse/ParsePlugins.java | 205 + .../src/main/java/com/parse/ParsePolygon.java | 228 + .../src/main/java/com/parse/ParsePush.java | 546 ++ .../com/parse/ParsePushBroadcastReceiver.java | 473 ++ .../parse/ParsePushChannelsController.java | 75 + .../java/com/parse/ParsePushController.java | 49 + .../src/main/java/com/parse/ParseQuery.java | 2195 +++++++++ .../java/com/parse/ParseQueryController.java | 51 + .../com/parse/ParseRESTAnalyticsCommand.java | 71 + .../java/com/parse/ParseRESTCloudCommand.java | 31 + .../main/java/com/parse/ParseRESTCommand.java | 539 ++ .../com/parse/ParseRESTConfigCommand.java | 40 + .../java/com/parse/ParseRESTFileCommand.java | 88 + .../parse/ParseRESTObjectBatchCommand.java | 170 + .../com/parse/ParseRESTObjectCommand.java | 70 + .../java/com/parse/ParseRESTPushCommand.java | 80 + .../java/com/parse/ParseRESTQueryCommand.java | 110 + .../com/parse/ParseRESTSessionCommand.java | 39 + .../java/com/parse/ParseRESTUserCommand.java | 129 + .../main/java/com/parse/ParseRelation.java | 288 ++ .../com/parse/ParseRelationOperation.java | 280 ++ .../java/com/parse/ParseRemoveOperation.java | 113 + .../src/main/java/com/parse/ParseRequest.java | 309 ++ .../src/main/java/com/parse/ParseRole.java | 141 + .../java/com/parse/ParseSQLiteCursor.java | 266 + .../java/com/parse/ParseSQLiteDatabase.java | 387 ++ .../java/com/parse/ParseSQLiteOpenHelper.java | 61 + .../src/main/java/com/parse/ParseSession.java | 125 + .../com/parse/ParseSessionController.java | 20 + .../java/com/parse/ParseSetOperation.java | 50 + .../main/java/com/parse/ParseTaskUtils.java | 135 + .../main/java/com/parse/ParseTextUtils.java | 64 + .../main/java/com/parse/ParseTraverser.java | 139 + .../src/main/java/com/parse/ParseUser.java | 1549 ++++++ .../java/com/parse/ParseUserController.java | 38 + .../java/com/parse/ParseUserCurrentCoder.java | 128 + .../main/java/com/parse/ParseWakeLock.java | 66 + .../main/java/com/parse/PointerEncoder.java | 35 + .../com/parse/PointerOrLocalIdEncoder.java | 46 + .../main/java/com/parse/ProgressCallback.java | 24 + .../src/main/java/com/parse/PushHandler.java | 125 + .../src/main/java/com/parse/PushHistory.java | 147 + .../src/main/java/com/parse/PushRouter.java | 167 + .../src/main/java/com/parse/PushService.java | 277 ++ .../main/java/com/parse/PushServiceApi26.java | 117 + .../main/java/com/parse/PushServiceUtils.java | 69 + .../src/main/java/com/parse/PushType.java | 43 + .../main/java/com/parse/RefreshCallback.java | 47 + .../parse/RequestPasswordResetCallback.java | 47 + .../src/main/java/com/parse/SaveCallback.java | 44 + .../src/main/java/com/parse/SendCallback.java | 47 + .../main/java/com/parse/SignUpCallback.java | 45 + .../src/main/java/com/parse/TaskQueue.java | 118 + .../com/parse/TaskStackBuilderHelper.java | 32 + .../main/java/com/parse/WeakValueHashMap.java | 53 + .../java/com/parse/http/ParseHttpBody.java | 74 + .../java/com/parse/http/ParseHttpRequest.java | 256 + .../com/parse/http/ParseHttpResponse.java | 248 + ...chedCurrentInstallationControllerTest.java | 250 + .../CachedCurrentUserControllerTest.java | 479 ++ .../java/com/parse/FileObjectStoreTest.java | 103 + .../java/com/parse/InstallationIdTest.java | 114 + .../src/test/java/com/parse/ListsTest.java | 43 + .../java/com/parse/LocalIdManagerTest.java | 94 + .../parse/NetworkObjectControllerTest.java | 269 + .../com/parse/NetworkQueryControllerTest.java | 174 + .../parse/NetworkSessionControllerTest.java | 129 + .../com/parse/NetworkUserControllerTest.java | 252 + .../com/parse/OfflineObjectStoreTest.java | 263 + .../com/parse/OfflineQueryControllerTest.java | 165 + .../java/com/parse/OfflineQueryLogicTest.java | 1178 +++++ .../src/test/java/com/parse/ParseACLTest.java | 690 +++ .../parse/ParseAnalyticsControllerTest.java | 127 + .../java/com/parse/ParseAnalyticsTest.java | 311 ++ .../parse/ParseAuthenticationManagerTest.java | 96 + .../com/parse/ParseByteArrayHttpBodyTest.java | 55 + .../parse/ParseClientConfigurationTest.java | 163 + .../parse/ParseCloudCodeControllerTest.java | 194 + .../test/java/com/parse/ParseCloudTest.java | 147 + .../test/java/com/parse/ParseCoderTest.java | 45 + .../com/parse/ParseConfigControllerTest.java | 199 + .../test/java/com/parse/ParseConfigTest.java | 1038 ++++ .../java/com/parse/ParseCorePluginsTest.java | 200 + .../ParseCountingByteArrayHttpBodyTest.java | 71 + .../parse/ParseCountingFileHttpBodyTest.java | 88 + .../ParseCurrentConfigControllerTest.java | 330 ++ .../java/com/parse/ParseDateFormatTest.java | 39 + .../test/java/com/parse/ParseDecoderTest.java | 333 ++ .../parse/ParseDefaultACLControllerTest.java | 162 + .../java/com/parse/ParseDigestUtilsTest.java | 33 + .../test/java/com/parse/ParseEncoderTest.java | 213 + .../com/parse/ParseFileControllerTest.java | 362 ++ .../java/com/parse/ParseFileHttpBodyTest.java | 92 + .../java/com/parse/ParseFileRequestTest.java | 59 + .../java/com/parse/ParseFileStateTest.java | 89 + .../test/java/com/parse/ParseFileTest.java | 531 ++ .../java/com/parse/ParseFileUtilsTest.java | 108 + .../java/com/parse/ParseGeoPointTest.java | 66 + .../java/com/parse/ParseHttpClientTest.java | 189 + .../java/com/parse/ParseHttpRequestTest.java | 121 + .../java/com/parse/ParseHttpResponseTest.java | 105 + .../parse/ParseImpreciseDateFormatTest.java | 39 + .../java/com/parse/ParseInstallationTest.java | 413 ++ .../com/parse/ParseKeyValueCacheTest.java | 98 + .../test/java/com/parse/ParseMatchers.java | 37 + .../parse/ParseObjectCurrentCoderTest.java | 198 + .../java/com/parse/ParseObjectStateTest.java | 171 + .../test/java/com/parse/ParseObjectTest.java | 793 +++ .../java/com/parse/ParseOkHttpClientTest.java | 238 + .../test/java/com/parse/ParsePolygonTest.java | 123 + .../com/parse/ParsePushControllerTest.java | 393 ++ .../java/com/parse/ParsePushStateTest.java | 502 ++ .../test/java/com/parse/ParsePushTest.java | 823 ++++ .../java/com/parse/ParseQueryStateTest.java | 309 ++ .../test/java/com/parse/ParseQueryTest.java | 907 ++++ .../java/com/parse/ParseRESTCommandTest.java | 534 ++ .../com/parse/ParseRESTQueryCommandTest.java | 129 + .../com/parse/ParseRESTUserCommandTest.java | 169 + .../java/com/parse/ParseRelationTest.java | 267 + .../test/java/com/parse/ParseRequestTest.java | 154 + .../test/java/com/parse/ParseRoleTest.java | 162 + .../test/java/com/parse/ParseSessionTest.java | 68 + .../java/com/parse/ParseTaskUtilsTest.java | 52 + .../test/java/com/parse/ParseTestUtils.java | 55 + .../java/com/parse/ParseTextUtilsTest.java | 83 + .../com/parse/ParseUserCurrentCoderTest.java | 126 + .../test/java/com/parse/ParseUserTest.java | 1544 ++++++ .../java/com/parse/PointerEncoderTest.java | 39 + .../test/java/com/parse/PushHandlerTest.java | 28 + .../test/java/com/parse/PushServiceTest.java | 105 + .../java/com/parse/PushServiceUtilsTest.java | 42 + .../java/com/parse/ResetPluginsParseTest.java | 24 + .../src/test/java/com/parse/SubclassTest.java | 170 + .../java/com/parse/TaskQueueTestHelper.java | 59 + .../src/test/java/com/parse/TestHelper.java | 9 + ExternalLibs/Parse-SDK-Android/README.md | 158 + ExternalLibs/Parse-SDK-Android/build.gradle | 32 + .../gradle/ExcludeDoclet.jar | Bin 0 -> 3539 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54783 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + ExternalLibs/Parse-SDK-Android/gradlew | 172 + ExternalLibs/Parse-SDK-Android/gradlew.bat | 84 + .../scripts/publish_snapshot.sh | 19 + .../Parse-SDK-Android/settings.gradle | 2 + .../ParseLiveQuery-Android/.gitignore | 40 + .../ParseLiveQuery-Android/.travis.yml | 42 + .../ParseLiveQuery-Android/CONTRIBUTING.md | 62 + ExternalLibs/ParseLiveQuery-Android/LICENSE | 34 + ExternalLibs/ParseLiveQuery-Android/PATENTS | 37 + .../ParseLiveQuery/build.gradle | 46 + .../ParseLiveQuery/release-proguard.pro | 29 + .../src/main/AndroidManifest.xml | 12 + .../main/java/com/parse/ClientOperation.java | 10 + .../com/parse/ConnectClientOperation.java | 24 + .../java/com/parse/LiveQueryException.java | 101 + .../com/parse/OkHttp3SocketClientFactory.java | 119 + .../java/com/parse/ParseLiveQueryClient.java | 47 + .../parse/ParseLiveQueryClientCallbacks.java | 11 + .../com/parse/ParseLiveQueryClientImpl.java | 435 ++ .../com/parse/SubscribeClientOperation.java | 38 + .../src/main/java/com/parse/Subscription.java | 120 + .../java/com/parse/SubscriptionHandling.java | 72 + .../com/parse/UnsubscribeClientOperation.java | 21 + .../main/java/com/parse/WebSocketClient.java | 27 + .../com/parse/WebSocketClientFactory.java | 9 + .../java/com/parse/ImmediateExecutor.java | 10 + .../com/parse/TestParseLiveQueryClient.java | 609 +++ ExternalLibs/ParseLiveQuery-Android/README.md | 131 + .../THIRD_PARTY_NOTICES.txt | 218 + .../ParseLiveQuery-Android/build.gradle | 27 + .../gradle/ExcludeDoclet.jar | Bin 0 -> 3539 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 50514 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + ExternalLibs/ParseLiveQuery-Android/gradlew | 164 + .../ParseLiveQuery-Android/gradlew.bat | 90 + .../scripts/publish_snapshot.sh | 19 + .../ParseLiveQuery-Android/settings.gradle | 1 + app/.gitignore | 1 + app/build.gradle | 36 + app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 36 + .../apps/parseapplication/MainActivity.kt | 15 + .../apps/parseapplication/ParseApplication.kt | 29 + .../apps/parseapplication/Utils/ParseUtils.kt | 9 + .../apps/parseapplication/Utils/Utils.kt | 9 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 171 + app/src/main/res/layout/activity_main.xml | 19 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/styles.xml | 11 + .../apps/parseapplication/ExampleUnitTest.kt | 17 + build.gradle | 37 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 + gradlew.bat | 90 + settings.gradle | 3 + 334 files changed, 56965 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .idea/dictionaries/Philippe_Adrien.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 ExternalLibs/Parse-SDK-Android/.codecov.yml create mode 100644 ExternalLibs/Parse-SDK-Android/.gitignore create mode 100644 ExternalLibs/Parse-SDK-Android/.travis.yml create mode 100644 ExternalLibs/Parse-SDK-Android/CONTRIBUTING.md create mode 100644 ExternalLibs/Parse-SDK-Android/LICENSE create mode 100644 ExternalLibs/Parse-SDK-Android/PATENTS create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/build.gradle create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/release-proguard.pro create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/AndroidManifest.xml create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AbstractQueryController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AuthenticationCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CacheQueryController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentInstallationController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentUserController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConfigCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConnectivityNotifier.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CountCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/DeleteCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/EventuallyPin.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FileObjectStore.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FindCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FunctionCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmPushHandler.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmRegistrar.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataStreamCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetFileCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/InstallationId.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/KnownParseObjectDecoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Lists.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocalIdManager.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationNotifier.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LockSet.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogInCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogOutCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ManifestInfo.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkObjectController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkQueryController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkSessionController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkUserController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NoObjectsEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NotificationCompat.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Numbers.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineObjectStore.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryLogic.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineSQLiteOpenHelper.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineStore.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PLog.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Parse.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseACL.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalytics.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalyticsController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnonymousUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAuthenticationManager.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseByteArrayHttpBody.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback1.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback2.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseClassName.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloud.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloudCodeController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCommandCache.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfig.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfigController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCorePlugins.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingByteArrayHttpBody.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingFileHttpBody.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentConfigController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentInstallationController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentUserController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDateFormat.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDecoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDefaultACLController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDeleteOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDigestUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEventuallyQueue.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseException.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseExecutors.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperations.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFile.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileHttpBody.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileRequest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseGeoPoint.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseHttpClient.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIOUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseImpreciseDateFormat.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIncrementOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseInstallation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseJSONUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseKeyValueCache.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseMulticastDelegate.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseNotificationManager.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObject.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentCoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectStore.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseOperationSet.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelDecoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePin.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePlugins.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePolygon.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePush.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushChannelsController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQuery.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQueryController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTAnalyticsCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCloudCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTConfigCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTFileCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTPushCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTQueryCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTSessionCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTUserCommand.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelationOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRemoveOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRequest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRole.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteCursor.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteDatabase.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteOpenHelper.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSession.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSessionController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSetOperation.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTaskUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTextUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTraverser.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUser.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserController.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserCurrentCoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseWakeLock.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ProgressCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHandler.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHistory.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushRouter.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushService.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceApi26.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushType.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RefreshCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RequestPasswordResetCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SaveCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SendCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SignUpCallback.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskQueue.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskStackBuilderHelper.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/WeakValueHashMap.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpBody.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpRequest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpResponse.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/FileObjectStoreTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/InstallationIdTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ListsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/LocalIdManagerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkObjectControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkQueryControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkSessionControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkUserControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineObjectStoreTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryLogicTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseACLTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseByteArrayHttpBodyTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseClientConfigurationTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCorePluginsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDateFormatTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDecoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDigestUtilsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseEncoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileHttpBodyTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileRequestTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileStateTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileUtilsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseGeoPointTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpClientTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpRequestTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpResponseTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseImpreciseDateFormatTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseInstallationTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseKeyValueCacheTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseMatchers.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectCurrentCoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectStateTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseOkHttpClientTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePolygonTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushControllerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushStateTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryStateTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTCommandTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTQueryCommandTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTUserCommandTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRelationTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRequestTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRoleTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseSessionTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTaskUtilsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTestUtils.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTextUtilsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserCurrentCoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PointerEncoderTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushHandlerTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceUtilsTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ResetPluginsParseTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/SubclassTest.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TaskQueueTestHelper.java create mode 100644 ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TestHelper.java create mode 100644 ExternalLibs/Parse-SDK-Android/README.md create mode 100644 ExternalLibs/Parse-SDK-Android/build.gradle create mode 100644 ExternalLibs/Parse-SDK-Android/gradle/ExcludeDoclet.jar create mode 100644 ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.jar create mode 100644 ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.properties create mode 100755 ExternalLibs/Parse-SDK-Android/gradlew create mode 100644 ExternalLibs/Parse-SDK-Android/gradlew.bat create mode 100755 ExternalLibs/Parse-SDK-Android/scripts/publish_snapshot.sh create mode 100644 ExternalLibs/Parse-SDK-Android/settings.gradle create mode 100644 ExternalLibs/ParseLiveQuery-Android/.gitignore create mode 100644 ExternalLibs/ParseLiveQuery-Android/.travis.yml create mode 100644 ExternalLibs/ParseLiveQuery-Android/CONTRIBUTING.md create mode 100644 ExternalLibs/ParseLiveQuery-Android/LICENSE create mode 100644 ExternalLibs/ParseLiveQuery-Android/PATENTS create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/build.gradle create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/release-proguard.pro create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/AndroidManifest.xml create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ClientOperation.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ConnectClientOperation.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/LiveQueryException.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/OkHttp3SocketClientFactory.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClient.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientCallbacks.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientImpl.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscribeClientOperation.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/Subscription.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscriptionHandling.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/UnsubscribeClientOperation.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClient.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClientFactory.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/ImmediateExecutor.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/TestParseLiveQueryClient.java create mode 100644 ExternalLibs/ParseLiveQuery-Android/README.md create mode 100644 ExternalLibs/ParseLiveQuery-Android/THIRD_PARTY_NOTICES.txt create mode 100644 ExternalLibs/ParseLiveQuery-Android/build.gradle create mode 100644 ExternalLibs/ParseLiveQuery-Android/gradle/ExcludeDoclet.jar create mode 100644 ExternalLibs/ParseLiveQuery-Android/gradle/wrapper/gradle-wrapper.jar create mode 100644 ExternalLibs/ParseLiveQuery-Android/gradle/wrapper/gradle-wrapper.properties create mode 100755 ExternalLibs/ParseLiveQuery-Android/gradlew create mode 100644 ExternalLibs/ParseLiveQuery-Android/gradlew.bat create mode 100755 ExternalLibs/ParseLiveQuery-Android/scripts/publish_snapshot.sh create mode 100644 ExternalLibs/ParseLiveQuery-Android/settings.gradle create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/net/adphi/apps/parseapplication/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/net/adphi/apps/parseapplication/MainActivity.kt create mode 100644 app/src/main/java/net/adphi/apps/parseapplication/ParseApplication.kt create mode 100644 app/src/main/java/net/adphi/apps/parseapplication/Utils/ParseUtils.kt create mode 100644 app/src/main/java/net/adphi/apps/parseapplication/Utils/Utils.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/net/adphi/apps/parseapplication/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..78266bd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "app/libs/ParseLiveQuery-Android"] + path = app/libs/ParseLiveQuery-Android + url = https://github.com/parse-community/ParseLiveQuery-Android.git +[submodule "app/libs/Parse-SDK-Android"] + path = app/libs/Parse-SDK-Android + url = https://github.com/parse-community/Parse-SDK-Android.git diff --git a/.idea/dictionaries/Philippe_Adrien.xml b/.idea/dictionaries/Philippe_Adrien.xml new file mode 100644 index 0000000..71ebc9f --- /dev/null +++ b/.idea/dictionaries/Philippe_Adrien.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..bd18d71 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b64a549 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + Android + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9836ad7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/ExternalLibs/Parse-SDK-Android/.codecov.yml b/ExternalLibs/Parse-SDK-Android/.codecov.yml new file mode 100644 index 0000000..ac4fc98 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/.codecov.yml @@ -0,0 +1,11 @@ +coverage: + precision: 2 + round: down + range: "45...100" + + status: + project: + default: + target: 45% + patch: yes + changes: no diff --git a/ExternalLibs/Parse-SDK-Android/.gitignore b/ExternalLibs/Parse-SDK-Android/.gitignore new file mode 100644 index 0000000..8cb1c31 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/.gitignore @@ -0,0 +1,40 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Android Studio +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Gradle +.gradle +build + +# Other +.metadata +*/bin/* +*/gen/* +testData +testCache +server.config + +# Jacoco +jacoco.exec + diff --git a/ExternalLibs/Parse-SDK-Android/.travis.yml b/ExternalLibs/Parse-SDK-Android/.travis.yml new file mode 100644 index 0000000..6cc9976 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/.travis.yml @@ -0,0 +1,35 @@ +branches: + only: + - master + - /^\d+\.\d+\.\d+$/ # regex + +language: android + +jdk: + - oraclejdk8 + +before_install: + - pip install --user codecov + - mkdir "$ANDROID_HOME/licenses" || true + - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" + +script: + - ./gradlew clean testDebugUnitTest jacocoTestReport + +after_success: + - ./gradlew coveralls + - codecov + - ./scripts/publish_snapshot.sh + +cache: + directories: + - $HOME/.gradle + - $HOME/.m2/repository + +deploy: + provider: script + script: ./gradlew bintrayUpload + skip_cleanup: true + on: + branch: master + tags: true diff --git a/ExternalLibs/Parse-SDK-Android/CONTRIBUTING.md b/ExternalLibs/Parse-SDK-Android/CONTRIBUTING.md new file mode 100644 index 0000000..5c46b12 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing to Parse SDK for Android +We want to make contributing to this project as easy and transparent as possible. + +## Code of Conduct +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. + +## Our Development Process +Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. + +### `master` is unsafe +Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. + +### Pull Requests +We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +1. Fork the repo and create your branch from `master`. +4. Add unit tests for any new code you add. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +### Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Bugs +Although we try to keep developing on Parse easy, you still may run into some issues. Technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. + +### Known Issues +We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues +Not all issues are SDK issues. If you're unsure whether your bug is with the SDK or backend, you can test to see if it reproduces with our [REST API][rest-api] and [Parse API Console][parse-api-console]. If it does, you can report backend bugs [here][bug-reports]. + +To view the REST API network requests issued by the Parse SDK and responses from the Parse backend, please check out [OkHttp Interceptors][network-debugging-tool]. With this tool, you can either log network requests/responses to Android logcat, or log them to Chrome Debugger via Stetho. + +Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: + +* A description. What did you expect to happen and what actually happened? Why do you think that was wrong? +* A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. +* What version does this reproduce on? What version did it last work on? +* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. +* Anything else you find relevant. + +### Security Bugs +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. + +## Style Guide +We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following: + +* Most importantly, match the existing code style as much as possible. +* Try to keep lines under 100 characters, if possible. + +## License +By contributing to Parse Android SDK, you agree that your contributions will be licensed under its license. + + [stack-overflow]: http://stackoverflow.com/tags/parse.com + [bug-reports]: https://github.com/parse-community/parse-server + [rest-api]: http://docs.parseplatform.org/rest/guide/ + [network-debugging-tool]: https://github.com/square/okhttp/wiki/Interceptors + [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ + [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg + [tests-dir]: /Parse/src/test/java/com/parse diff --git a/ExternalLibs/Parse-SDK-Android/LICENSE b/ExternalLibs/Parse-SDK-Android/LICENSE new file mode 100644 index 0000000..de40bfc --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/LICENSE @@ -0,0 +1,34 @@ +BSD License + +For Parse Android SDK software + +Copyright (c) 2015-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/ExternalLibs/Parse-SDK-Android/PATENTS b/ExternalLibs/Parse-SDK-Android/PATENTS new file mode 100644 index 0000000..2b6956a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/PATENTS @@ -0,0 +1,37 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the Parse Android SDK software distributed by Parse, LLC. + +Parse, LLC. ("Parse") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Parse’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Parse or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Parse or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Parse or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Parse that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/ExternalLibs/Parse-SDK-Android/Parse/build.gradle b/ExternalLibs/Parse-SDK-Android/Parse/build.gradle new file mode 100644 index 0000000..2a95ed0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.github.kt3k.coveralls' + +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.1' + } +} + +android { + compileSdkVersion 26 + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName project.version + consumerProguardFiles 'release-proguard.pro' + } + + packagingOptions { + exclude '**/BuildConfig.class' + } + + lintOptions { + abortOnError false + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +//ext { +// okhttpVersion = '3.9.1' +//} + +dependencies { + api "com.android.support:support-annotations:$supportLibVersion" + api 'com.parse.bolts:bolts-tasks:1.4.0' + api "com.squareup.okhttp3:okhttp:$okhttpVersion" + testImplementation 'org.robolectric:robolectric:3.3.2' + testImplementation 'org.skyscreamer:jsonassert:1.5.0' + testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" +} \ No newline at end of file diff --git a/ExternalLibs/Parse-SDK-Android/Parse/release-proguard.pro b/ExternalLibs/Parse-SDK-Android/Parse/release-proguard.pro new file mode 100644 index 0000000..01ea165 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/release-proguard.pro @@ -0,0 +1,7 @@ +-keepnames class com.parse.** { *; } + +# Required for Parse +-keepattributes *Annotation* +-keepattributes Signature +# https://github.com/square/okio#proguard +-dontwarn okio.** diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/AndroidManifest.xml b/ExternalLibs/Parse-SDK-Android/Parse/src/main/AndroidManifest.xml new file mode 100644 index 0000000..255fb58 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AbstractQueryController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AbstractQueryController.java new file mode 100644 index 0000000..c4d73eb --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AbstractQueryController.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** + * {@code AbstractParseQueryController} is an abstract implementation of + * {@link ParseQueryController}, which implements {@link ParseQueryController#getFirstAsync}. + */ +/** package */ abstract class AbstractQueryController implements ParseQueryController { + + @Override + public Task getFirstAsync(ParseQuery.State state, ParseUser user, + Task cancellationToken) { + return findAsync(state, user, cancellationToken).continueWith(new Continuation, T>() { + @Override + public T then(Task> task) throws Exception { + if (task.isFaulted()) { + throw task.getError(); + } + if (task.getResult() != null && task.getResult().size() > 0) { + return task.getResult().get(0); + } + throw new ParseException(ParseException.OBJECT_NOT_FOUND, "no results found for query"); + } + }); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AuthenticationCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AuthenticationCallback.java new file mode 100644 index 0000000..396a94e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/AuthenticationCallback.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Map; + +/** + * Provides a general interface for delegation of third party authentication callbacks. + */ +public interface AuthenticationCallback { + /** + * Called when restoring third party authentication credentials that have been serialized, + * such as session keys, etc. + *

+ * Note: This will be executed on a background thread. + * + * @param authData + * The auth data for the provider. This value may be {@code null} when + * unlinking an account. + * + * @return {@code true} iff the {@code authData} was successfully synchronized or {@code false} + * if user should no longer be associated because of bad {@code authData}. + */ + boolean onRestore(Map authData); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CacheQueryController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CacheQueryController.java new file mode 100644 index 0000000..ff78485 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CacheQueryController.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.concurrent.Callable; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class CacheQueryController extends AbstractQueryController { + + private final NetworkQueryController networkController; + + public CacheQueryController(NetworkQueryController network) { + networkController = network; + } + + @Override + public Task> findAsync( + final ParseQuery.State state, + final ParseUser user, + final Task cancellationToken) { + final String sessionToken = user != null ? user.getSessionToken() : null; + CommandDelegate> callbacks = new CommandDelegate>() { + @Override + public Task> runOnNetworkAsync() { + return networkController.findAsync(state, sessionToken, cancellationToken); + } + + @Override + public Task> runFromCacheAsync() { + return findFromCacheAsync(state, sessionToken); + } + }; + return runCommandWithPolicyAsync(callbacks, state.cachePolicy()); + } + + @Override + public Task countAsync( + final ParseQuery.State state, + final ParseUser user, + final Task cancellationToken) { + final String sessionToken = user != null ? user.getSessionToken() : null; + CommandDelegate callbacks = new CommandDelegate() { + @Override + public Task runOnNetworkAsync() { + return networkController.countAsync(state, sessionToken, cancellationToken); + } + + @Override + public Task runFromCacheAsync() { + return countFromCacheAsync(state, sessionToken); + } + }; + return runCommandWithPolicyAsync(callbacks, state.cachePolicy()); + } + + /** + * Retrieves the results of the last time {@link ParseQuery#find()} was called on a query + * identical to this one. + * + * @param sessionToken The user requesting access. + * @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no + * cache for this query. + */ + private Task> findFromCacheAsync( + final ParseQuery.State state, String sessionToken) { + final String cacheKey = ParseRESTQueryCommand.findCommand(state, sessionToken).getCacheKey(); + return Task.call(new Callable>() { + @Override + public List call() throws Exception { + JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()); + if (cached == null) { + throw new ParseException(ParseException.CACHE_MISS, "results not cached"); + } + try { + return networkController.convertFindResponse(state, cached); + } catch (JSONException e) { + throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json"); + } + } + }, Task.BACKGROUND_EXECUTOR); + } + + /** + * Retrieves the results of the last time {@link ParseQuery#count()} was called on a query + * identical to this one. + * + * @param sessionToken The user requesting access. + * @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no + * cache for this query. + */ + private Task countFromCacheAsync( + final ParseQuery.State state, String sessionToken) { + final String cacheKey = ParseRESTQueryCommand.countCommand(state, sessionToken).getCacheKey(); + return Task.call(new Callable() { + @Override + public Integer call() throws Exception { + JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()); + if (cached == null) { + throw new ParseException(ParseException.CACHE_MISS, "results not cached"); + } + try { + return cached.getInt("count"); + } catch (JSONException e) { + throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json"); + } + } + }, Task.BACKGROUND_EXECUTOR); + } + + private Task runCommandWithPolicyAsync(final CommandDelegate c, + ParseQuery.CachePolicy policy) { + switch (policy) { + case IGNORE_CACHE: + case NETWORK_ONLY: + return c.runOnNetworkAsync(); + case CACHE_ONLY: + return c.runFromCacheAsync(); + case CACHE_ELSE_NETWORK: + return c.runFromCacheAsync().continueWithTask(new Continuation>() { + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Override + public Task then(Task task) throws Exception { + if (task.getError() instanceof ParseException) { + return c.runOnNetworkAsync(); + } + return task; + } + }); + case NETWORK_ELSE_CACHE: + return c.runOnNetworkAsync().continueWithTask(new Continuation>() { + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Override + public Task then(Task task) throws Exception { + Exception error = task.getError(); + if (error instanceof ParseException && + ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { + return c.runFromCacheAsync(); + } + // Either the query succeeded, or there was an an error with the query, not the + // network + return task; + } + }); + case CACHE_THEN_NETWORK: + throw new RuntimeException( + "You cannot use the cache policy CACHE_THEN_NETWORK with find()"); + default: + throw new RuntimeException("Unknown cache policy: " + policy); + } + } + + /** + * A callback that will be used to tell runCommandWithPolicy how to perform the command on the + * network and form the cache. + */ + private interface CommandDelegate { + // Fetches data from the network. + Task runOnNetworkAsync(); + + // Fetches data from the cache. + Task runFromCacheAsync(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentInstallationController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentInstallationController.java new file mode 100644 index 0000000..79f8938 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentInstallationController.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class CachedCurrentInstallationController + implements ParseCurrentInstallationController { + + /* package */ static final String TAG = "com.parse.CachedCurrentInstallationController"; + + /* + * Note about lock ordering: + * + * You must NOT acquire the ParseInstallation instance mutex (the "mutex" field in ParseObject) + * while holding this current installation lock. (We used to use the ParseInstallation.class lock, + * but moved on to an explicit lock object since anyone could acquire the ParseInstallation.class + * lock as ParseInstallation is a public class.) Acquiring the instance mutex while holding this + * current installation lock will lead to a deadlock. Here is an example: + * https://phabricator.fb.com/P3251091 + */ + private final Object mutex = new Object(); + + private final TaskQueue taskQueue = new TaskQueue(); + + private final ParseObjectStore store; + private final InstallationId installationId; + + // The "current installation" is the installation for this device. Protected by + // mutex. + /* package for test */ ParseInstallation currentInstallation; + + public CachedCurrentInstallationController( + ParseObjectStore store, InstallationId installationId) { + this.store = store; + this.installationId = installationId; + } + + @Override + public Task setAsync(final ParseInstallation installation) { + if (!isCurrent(installation)) { + return Task.forResult(null); + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.setAsync(installation); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + installationId.set(installation.getInstallationId()); + return task; + } + }, ParseExecutors.io()); + } + }); + } + + @Override + public Task getAsync() { + synchronized (mutex) { + if (currentInstallation != null) { + return Task.forResult(currentInstallation); + } + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (mutex) { + if (currentInstallation != null) { + return Task.forResult(currentInstallation); + } + } + + return store.getAsync().continueWith(new Continuation() { + @Override + public ParseInstallation then(Task task) throws Exception { + ParseInstallation current = task.getResult(); + if (current == null) { + current = ParseObject.create(ParseInstallation.class); + current.updateDeviceInfo(installationId); + } else { + installationId.set(current.getInstallationId()); + PLog.v(TAG, "Successfully deserialized Installation object"); + } + + synchronized (mutex) { + currentInstallation = current; + } + return current; + } + }, ParseExecutors.io()); + } + }); + } + }); + } + + @Override + public Task existsAsync() { + synchronized (mutex) { + if (currentInstallation != null) { + return Task.forResult(true); + } + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.existsAsync(); + } + }); + } + }); + } + + @Override + public void clearFromMemory() { + synchronized (mutex) { + currentInstallation = null; + } + } + + @Override + public void clearFromDisk() { + synchronized (mutex) { + currentInstallation = null; + } + try { + installationId.clear(); + ParseTaskUtils.wait(store.deleteAsync()); + } catch (ParseException e) { + // ignored + } + } + + @Override + public boolean isCurrent(ParseInstallation installation) { + synchronized (mutex) { + return currentInstallation == installation; + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentUserController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentUserController.java new file mode 100644 index 0000000..077de9e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CachedCurrentUserController.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Arrays; +import java.util.Map; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class CachedCurrentUserController implements ParseCurrentUserController { + + /** + * Lock used to synchronize current user modifications and access. + * + * Note about lock ordering: + * + * You must NOT acquire the ParseUser instance mutex (the "mutex" field in ParseObject) while + * holding this static initialization lock. Doing so will cause a deadlock. Here's an example: + * https://phabricator.fb.com/P17182641 + */ + private final Object mutex = new Object(); + private final TaskQueue taskQueue = new TaskQueue(); + + private final ParseObjectStore store; + + /* package */ ParseUser currentUser; + // Whether currentUser is known to match the serialized version on disk. This is useful for saving + // a filesystem check if you try to load currentUser frequently while there is none on disk. + /* package */ boolean currentUserMatchesDisk = false; + + public CachedCurrentUserController(ParseObjectStore store) { + this.store = store; + } + + @Override + public Task setAsync(final ParseUser user) { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser oldCurrentUser; + synchronized (mutex) { + oldCurrentUser = currentUser; + } + + if (oldCurrentUser != null && oldCurrentUser != user) { + // We don't need to revoke the token since we're not explicitly calling logOut + // We don't need to remove persisted files since we're overwriting them + return oldCurrentUser.logOutAsync(false).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + return null; // ignore errors + } + }); + } + return task; + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + user.setIsCurrentUser(true); + return user.synchronizeAllAuthDataAsync(); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.setAsync(user).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + synchronized (mutex) { + currentUserMatchesDisk = !task.isFaulted(); + currentUser = user; + } + return null; + } + }); + } + }); + } + }); + } + + @Override + public Task setIfNeededAsync(ParseUser user) { + synchronized (mutex) { + if (!user.isCurrentUser() || currentUserMatchesDisk) { + return Task.forResult(null); + } + } + + return setAsync(user); + } + + @Override + public Task getAsync() { + return getAsync(ParseUser.isAutomaticUserEnabled()); + } + + @Override + public Task existsAsync() { + synchronized (mutex) { + if (currentUser != null) { + return Task.forResult(true); + } + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.existsAsync(); + } + }); + } + }); + } + + @Override + public boolean isCurrent(ParseUser user) { + synchronized (mutex) { + return currentUser == user; + } + } + + @Override + public void clearFromMemory() { + synchronized (mutex) { + currentUser = null; + currentUserMatchesDisk = false; + } + } + + @Override + public void clearFromDisk() { + synchronized (mutex) { + currentUser = null; + currentUserMatchesDisk = false; + } + try { + ParseTaskUtils.wait(store.deleteAsync()); + } catch (ParseException e) { + // ignored + } + } + + @Override + public Task getCurrentSessionTokenAsync() { + return getAsync(false).onSuccess(new Continuation() { + @Override + public String then(Task task) throws Exception { + ParseUser user = task.getResult(); + return user != null ? user.getSessionToken() : null; + } + }); + } + + @Override + public Task logOutAsync() { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + // We can parallelize disk and network work, but only after we restore the current user from + // disk. + final Task userTask = getAsync(false); + return Task.whenAll(Arrays.asList(userTask, toAwait)).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + Task logOutTask = userTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser user = task.getResult(); + if (user == null) { + return task.cast(); + } + return user.logOutAsync(); + } + }); + + Task diskTask = store.deleteAsync().continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + boolean deleted = !task.isFaulted(); + synchronized (mutex) { + currentUserMatchesDisk = deleted; + currentUser = null; + } + return null; + } + }); + return Task.whenAll(Arrays.asList(logOutTask, diskTask)); + } + }); + } + }); + } + + @Override + public Task getAsync(final boolean shouldAutoCreateUser) { + synchronized (mutex) { + if (currentUser != null) { + return Task.forResult(currentUser); + } + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task ignored) throws Exception { + ParseUser current; + boolean matchesDisk; + synchronized (mutex) { + current = currentUser; + matchesDisk = currentUserMatchesDisk; + } + + if (current != null) { + return Task.forResult(current); + } + + if (matchesDisk) { + if (shouldAutoCreateUser) { + return Task.forResult(lazyLogIn()); + } + return null; + } + + return store.getAsync().continueWith(new Continuation() { + @Override + public ParseUser then(Task task) throws Exception { + ParseUser current = task.getResult(); + boolean matchesDisk = !task.isFaulted(); + + synchronized (mutex) { + currentUser = current; + currentUserMatchesDisk = matchesDisk; + } + + if (current != null) { + synchronized (current.mutex) { + current.setIsCurrentUser(true); + } + return current; + } + + if (shouldAutoCreateUser) { + return lazyLogIn(); + } + return null; + } + }); + } + }); + } + }); + } + + private ParseUser lazyLogIn() { + Map authData = ParseAnonymousUtils.getAuthData(); + return lazyLogIn(ParseAnonymousUtils.AUTH_TYPE, authData); + } + + /* package for tests */ ParseUser lazyLogIn(String authType, Map authData) { + // Note: if authType != ParseAnonymousUtils.AUTH_TYPE the user is not "lazy". + ParseUser user = ParseObject.create(ParseUser.class); + synchronized (user.mutex) { + user.setIsCurrentUser(true); + user.putAuthData(authType, authData); + } + + synchronized (mutex) { + currentUserMatchesDisk = false; + currentUser = user; + } + + return user; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConfigCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConfigCallback.java new file mode 100644 index 0000000..2a84d4f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConfigCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code ConfigCallback} is used to run code after {@link ParseConfig#getInBackground()} is used + * to fetch a new configuration object from the server in a background thread. + *

+ * The easiest way to use a {@code ConfigCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the fetch is complete. + * The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * ParseConfig.getInBackground(new ConfigCallback() {
+ *   public void done(ParseConfig config, ParseException e) {
+ *     if (e == null) {
+ *       configFetchSuccess(object);
+ *     } else {
+ *       configFetchFailed(e);
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface ConfigCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param config + * A new {@code ParseConfig} instance from the server, or {@code null} if it did not + * succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + void done(ParseConfig config, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConnectivityNotifier.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConnectivityNotifier.java new file mode 100644 index 0000000..c43e240 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ConnectivityNotifier.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ReceiverCallNotAllowedException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** package */ class ConnectivityNotifier extends BroadcastReceiver { + private static final String TAG = "com.parse.ConnectivityNotifier"; + public interface ConnectivityListener { + void networkConnectivityStatusChanged(Context context, Intent intent); + } + + private static final ConnectivityNotifier singleton = new ConnectivityNotifier(); + public static ConnectivityNotifier getNotifier(Context context) { + singleton.tryToRegisterForNetworkStatusNotifications(context); + return singleton; + } + + public static boolean isConnected(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null) { + return false; + } + + NetworkInfo network = connectivityManager.getActiveNetworkInfo(); + return network != null && network.isConnected(); + } + + private Set listeners = new HashSet<>(); + private boolean hasRegisteredReceiver = false; + private final Object lock = new Object(); + + public void addListener(ConnectivityListener delegate) { + synchronized (lock) { + listeners.add(delegate); + } + } + + public void removeListener(ConnectivityListener delegate) { + synchronized (lock) { + listeners.remove(delegate); + } + } + + private boolean tryToRegisterForNetworkStatusNotifications(Context context) { + synchronized (lock) { + if (hasRegisteredReceiver) { + return true; + } + + try { + if (context == null) { + return false; + } + context = context.getApplicationContext(); + context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + hasRegisteredReceiver = true; + return true; + } catch (ReceiverCallNotAllowedException e) { + // In practice, this only happens with the push service, which will trigger a retry soon afterwards. + PLog.v(TAG, "Cannot register a broadcast receiver because the executing " + + "thread is currently in a broadcast receiver. Will try again later."); + return false; + } + } + } + + @Override + public void onReceive(Context context, Intent intent) { + List listenersCopy; + synchronized (lock) { + listenersCopy = new ArrayList<>(listeners); + } + for (ConnectivityListener delegate : listenersCopy) { + delegate.networkConnectivityStatusChanged(context, intent); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CountCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CountCallback.java new file mode 100644 index 0000000..48b9234 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/CountCallback.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code CountCallback} is used to run code after a {@link ParseQuery} is used to count objects + * matching a query in a background thread. + *

+ * The easiest way to use a {@code CountCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the count is complete. + * The {@code done} function will be run in the UI thread, while the count happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ * For example, this sample code counts objects of class {@code "MyClass"}. It calls a + * different function depending on whether the count succeeded or not. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.countInBackground(new CountCallback() {
+ *   public void done(int count, ParseException e) {
+ *     if (e == null) {
+ *       objectsWereCountedSuccessfully(count);
+ *     } else {
+ *       objectCountingFailed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +// FYI, this does not extend ParseCallback2 since the first param is `int`, which can't be used +// in a generic. +public interface CountCallback { + /** + * Override this function with the code you want to run after the count is complete. + * + * @param count + * The number of objects matching the query, or -1 if it failed. + * @param e + * The exception raised by the count, or null if it succeeded. + */ + void done(int count, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/DeleteCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/DeleteCallback.java new file mode 100644 index 0000000..ee8f725 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/DeleteCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code DeleteCallback} is used to run code after saving a {@link ParseObject} in a background + * thread. + *

+ * The easiest way to use a {@code DeleteCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the delete is complete. + * The {@code done} function will be run in the UI thread, while the delete happens in a + * background thread. This ensures that the UI does not freeze while the delete happens. + *

+ * For example, this sample code deletes the object {@code myObject} and calls a different + * function depending on whether the save succeeded or not. + *

+ *

+ * myObject.deleteInBackground(new DeleteCallback() {
+ *   public void done(ParseException e) {
+ *     if (e == null) {
+ *       myObjectWasDeletedSuccessfully();
+ *     } else {
+ *       myObjectDeleteDidNotSucceed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface DeleteCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the delete is complete. + * + * @param e + * The exception raised by the delete, or {@code null} if it succeeded. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/EventuallyPin.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/EventuallyPin.java new file mode 100644 index 0000000..a7b7b24 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/EventuallyPin.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import bolts.Continuation; +import bolts.Task; + +/** + * Properties + * - time + * Used for sort order when querying for all EventuallyPins + * - type + * TYPE_SAVE or TYPE_DELETE + * - object + * The object that the operation should notify when complete + * - operationSetUUID + * The operationSet to be completed + * - sessionToken + * The user that instantiated the operation + */ +@ParseClassName("_EventuallyPin") +/** package */ class EventuallyPin extends ParseObject { + + public static final String PIN_NAME = "_eventuallyPin"; + + public static final int TYPE_SAVE = 1; + public static final int TYPE_DELETE = 2; + public static final int TYPE_COMMAND = 3; + + public EventuallyPin() { + super("_EventuallyPin"); + } + + @Override + boolean needsDefaultACL() { + return false; + } + + public String getUUID() { + return getString("uuid"); + } + + public int getType() { + return getInt("type"); + } + + public ParseObject getObject() { + return getParseObject("object"); + } + + public String getOperationSetUUID() { + return getString("operationSetUUID"); + } + + public String getSessionToken() { + return getString("sessionToken"); + } + + public ParseRESTCommand getCommand() throws JSONException { + JSONObject json = getJSONObject("command"); + ParseRESTCommand command = null; + if (ParseRESTCommand.isValidCommandJSONObject(json)) { + command = ParseRESTCommand.fromJSONObject(json); + } else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) { + // do nothing + } else { + throw new JSONException("Failed to load command from JSON."); + } + return command; + } + + public static Task pinEventuallyCommand(ParseObject object, + ParseRESTCommand command) { + int type = TYPE_COMMAND; + JSONObject json = null; + if (command.httpPath.startsWith("classes")) { + if (command.method == ParseHttpRequest.Method.POST || + command.method == ParseHttpRequest.Method.PUT) { + type = TYPE_SAVE; + } else if (command.method == ParseHttpRequest.Method.DELETE) { + type = TYPE_DELETE; + } + } else { + json = command.toJSONObject(); + } + return pinEventuallyCommand( + type, + object, + command.getOperationSetUUID(), + command.getSessionToken(), + json); + } + + /** + * @param type + * Type of the command: TYPE_SAVE, TYPE_DELETE, TYPE_COMMAND + * @param obj + * (Optional) Object the operation is being executed on. Required for TYPE_SAVE and + * TYPE_DELETE. + * @param operationSetUUID + * (Optional) UUID of the ParseOperationSet that is paired with the ParseCommand. + * Required for TYPE_SAVE and TYPE_DELETE. + * @param sessionToken + * (Optional) The sessionToken for the command. Required for TYPE_SAVE and TYPE_DELETE. + * @param command + * (Optional) JSON representation of the ParseCommand. Required for TYPE_COMMAND. + * @return + * Returns a task that is resolved when the command is pinned. + */ + private static Task pinEventuallyCommand(int type, ParseObject obj, + String operationSetUUID, String sessionToken, JSONObject command) { + final EventuallyPin pin = new EventuallyPin(); + pin.put("uuid", UUID.randomUUID().toString()); + pin.put("time", new Date()); + pin.put("type", type); + if (obj != null) { + pin.put("object", obj); + } + if (operationSetUUID != null) { + pin.put("operationSetUUID", operationSetUUID); + } + if (sessionToken != null) { + pin.put("sessionToken", sessionToken); + } + if (command != null) { + pin.put("command", command); + } + return pin.pinInBackground(PIN_NAME).continueWith(new Continuation() { + @Override + public EventuallyPin then(Task task) throws Exception { + return pin; + } + }); + } + + public static Task> findAllPinned() { + return findAllPinned(null); + } + + public static Task> findAllPinned(Collection excludeUUIDs) { + ParseQuery query = new ParseQuery<>(EventuallyPin.class) + .fromPin(PIN_NAME) + .ignoreACLs() + .orderByAscending("time"); + + if (excludeUUIDs != null) { + query.whereNotContainedIn("uuid", excludeUUIDs); + } + + // We need pass in a null user because we don't want the query to fetch the current user + // from LDS. + return query.findInBackground().continueWithTask(new Continuation, Task>>() { + @Override + public Task> then(Task> task) throws Exception { + final List pins = task.getResult(); + List> tasks = new ArrayList<>(); + + for (EventuallyPin pin : pins) { + ParseObject object = pin.getObject(); + if (object != null) { + tasks.add(object.fetchFromLocalDatastoreAsync().makeVoid()); + } + } + + return Task.whenAll(tasks).continueWithTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + return Task.forResult(pins); + } + }); + } + }); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FileObjectStore.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FileObjectStore.java new file mode 100644 index 0000000..8bb8a8f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FileObjectStore.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Callable; + +import bolts.Task; + +/** package */ class FileObjectStore implements ParseObjectStore { + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + /** + * Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. + * + * @param coder + * Current coder to encode the ParseObject. + * @param current + * ParseObject which needs to be saved to disk. + * @param file + * The file to save the object to. + * + * @see #getFromDisk(ParseObjectCurrentCoder, File, ParseObject.State.Init) + */ + private static void saveToDisk( + ParseObjectCurrentCoder coder, ParseObject current, File file) { + JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get()); + try { + ParseFileUtils.writeJSONObjectToFile(file, json); + } catch (IOException e) { + //TODO(grantland): We should do something if this fails... + } + } + + /** + * Retrieves a {@code ParseObject} from a file on disk in /2/ format. + * + * @param coder + * Current coder to decode the ParseObject. + * @param file + * The file to retrieve the object from. + * @param builder + * An empty builder which is used to generate a empty state and rebuild a ParseObject. + * @return The {@code ParseObject} that was retrieved. If the file wasn't found, or the contents + * of the file is an invalid {@code ParseObject}, returns {@code null}. + * + * @see #saveToDisk(ParseObjectCurrentCoder, ParseObject, File) + */ + private static T getFromDisk( + ParseObjectCurrentCoder coder, File file, ParseObject.State.Init builder) { + JSONObject json; + try { + json = ParseFileUtils.readFileToJSONObject(file); + } catch (IOException | JSONException e) { + return null; + } + + ParseObject.State newState = coder.decode(builder, json, ParseDecoder.get()) + .isComplete(true) + .build(); + return ParseObject.from(newState); + } + + private final String className; + private final File file; + private final ParseObjectCurrentCoder coder; + + public FileObjectStore(Class clazz, File file, ParseObjectCurrentCoder coder) { + this(getSubclassingController().getClassName(clazz), file, coder); + } + + public FileObjectStore(String className, File file, ParseObjectCurrentCoder coder) { + this.className = className; + this.file = file; + this.coder = coder; + } + + @Override + public Task setAsync(final T object) { + return Task.call(new Callable() { + @Override + public Void call() throws Exception { + saveToDisk(coder, object, file); + //TODO (grantland): check to see if this failed? We currently don't for legacy reasons. + return null; + } + }, ParseExecutors.io()); + } + + @Override + public Task getAsync() { + return Task.call(new Callable() { + @Override + public T call() throws Exception { + if (!file.exists()) { + return null; + } + return getFromDisk(coder, file, ParseObject.State.newBuilder(className)); + } + }, ParseExecutors.io()); + } + + @Override + public Task existsAsync() { + return Task.call(new Callable() { + @Override + public Boolean call() throws Exception { + return file.exists(); + } + }, ParseExecutors.io()); + } + + @Override + public Task deleteAsync() { + return Task.call(new Callable() { + @Override + public Void call() throws Exception { + if (file.exists() && !ParseFileUtils.deleteQuietly(file)) { + throw new RuntimeException("Unable to delete"); + } + + return null; + } + }, ParseExecutors.io()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FindCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FindCallback.java new file mode 100644 index 0000000..a5ebff3 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FindCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +/** + * A {@code FindCallback} is used to run code after a {@link ParseQuery} is used to fetch a list of + * {@link ParseObject}s in a background thread. + *

+ * The easiest way to use a {@code FindCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the fetch is complete. + * The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ * For example, this sample code fetches all objects of class {@code "MyClass"}. It calls a + * different function depending on whether the fetch succeeded or not. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.findInBackground(new FindCallback<ParseObject>() {
+ *   public void done(List<ParseObject> objects, ParseException e) {
+ *     if (e == null) {
+ *       objectsWereRetrievedSuccessfully(objects);
+ *     } else {
+ *       objectRetrievalFailed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface FindCallback extends ParseCallback2, ParseException> { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param objects + * The objects that were retrieved, or null if it did not succeed. + * @param e + * The exception raised by the save, or null if it succeeded. + */ + @Override + void done(List objects, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FunctionCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FunctionCallback.java new file mode 100644 index 0000000..7a62693 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/FunctionCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code FunctionCallback} is used to run code after {@link ParseCloud#callFunction} is used to + * run a Cloud Function in a background thread. + *

+ * The easiest way to use a {@code FunctionCallback} is through an anonymous inner class. Override + * the {@code done} function to specify what the callback should do after the cloud function is + * complete. The {@code done} function will be run in the UI thread, while the fetch happens in + * a background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ * For example, this sample code calls a cloud function {@code "MyFunction"} with + * {@code params} and calls a different function depending on whether the function succeeded. + *

+ *

+ * ParseCloud.callFunctionInBackground("MyFunction"new, params, FunctionCallback() {
+ *   public void done(ParseObject object, ParseException e) {
+ *     if (e == null) {
+ *       cloudFunctionSucceeded(object);
+ *     } else {
+ *       cloudFunctionFailed();
+ *     }
+ *   }
+ * });
+ * 
+ * + * @param + * The type of object returned by the Cloud Function. + */ +public interface FunctionCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the cloud function is complete. + * + * @param object + * The object that was returned by the cloud function. + * @param e + * The exception raised by the cloud call, or {@code null} if it succeeded. + */ + @Override + void done(T object, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java new file mode 100644 index 0000000..7aed1bd --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.CallSuper; + +/** + * @exclude + */ +public class GcmBroadcastReceiver extends BroadcastReceiver { + @Override + @CallSuper + public void onReceive(Context context, Intent intent) { + PushServiceUtils.runService(context, intent); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmPushHandler.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmPushHandler.java new file mode 100644 index 0000000..8b7a86e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmPushHandler.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import bolts.Task; + +/** + * Proxy Service while running in GCM mode. + * + * We use an {@link ExecutorService} so that we can operate like a ghetto + * {@link android.app.IntentService} where all incoming {@link Intent}s will be handled + * sequentially. + */ +/** package */ class GcmPushHandler implements PushHandler { + private static final String TAG = "GcmPushHandler"; + + static final String REGISTER_RESPONSE_ACTION = "com.google.android.c2dm.intent.REGISTRATION"; + static final String RECEIVE_PUSH_ACTION = "com.google.android.c2dm.intent.RECEIVE"; + + GcmPushHandler() {} + + @NonNull + @Override + public SupportLevel isSupported() { + if (!ManifestInfo.isGooglePlayServicesAvailable()) { + return SupportLevel.MISSING_REQUIRED_DECLARATIONS; + } + return getManifestSupportLevel(); + } + + private SupportLevel getManifestSupportLevel() { + Context context = Parse.getApplicationContext(); + String[] requiredPermissions = new String[] { + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.WAKE_LOCK", + "com.google.android.c2dm.permission.RECEIVE", + context.getPackageName() + ".permission.C2D_MESSAGE" + }; + + if (!ManifestInfo.hasRequestedPermissions(context, requiredPermissions)) { + return SupportLevel.MISSING_REQUIRED_DECLARATIONS; + } + + String packageName = context.getPackageName(); + String rcvrPermission = "com.google.android.c2dm.permission.SEND"; + Intent[] intents = new Intent[] { + new Intent(GcmPushHandler.RECEIVE_PUSH_ACTION) + .setPackage(packageName) + .addCategory(packageName), + new Intent(GcmPushHandler.REGISTER_RESPONSE_ACTION) + .setPackage(packageName) + .addCategory(packageName), + }; + + if (!ManifestInfo.checkReceiver(GcmBroadcastReceiver.class, rcvrPermission, intents)) { + return SupportLevel.MISSING_REQUIRED_DECLARATIONS; + } + + String[] optionalPermissions = new String[] { + "android.permission.VIBRATE" + }; + + if (!ManifestInfo.hasGrantedPermissions(context, optionalPermissions)) { + return SupportLevel.MISSING_OPTIONAL_DECLARATIONS; + } + + return SupportLevel.SUPPORTED; + } + + @Nullable + @Override + public String getWarningMessage(SupportLevel level) { + switch (level) { + case SUPPORTED: return null; + case MISSING_OPTIONAL_DECLARATIONS: return "Using GCM for Parse Push, " + + "but the app manifest is missing some optional " + + "declarations that should be added for maximum reliability. Please " + + getWarningMessage(); + case MISSING_REQUIRED_DECLARATIONS: + if (ManifestInfo.isGooglePlayServicesAvailable()) { + return "Cannot use GCM for push because the app manifest is missing some " + + "required declarations. Please " + getWarningMessage(); + } else { + return "Cannot use GCM for push on this device because Google Play " + + "Services is not available. Install Google Play Services from the Play Store."; + } + } + return null; + } + + static String getWarningMessage() { + String packageName = Parse.getApplicationContext().getPackageName(); + String gcmPackagePermission = packageName + ".permission.C2D_MESSAGE"; + return "make sure that these permissions are declared as children " + + "of the root element:\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "Also, please make sure that these services and broadcast receivers are declared as " + + "children of the element:\n" + + "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + } + + @Override + public Task initialize() { + return GcmRegistrar.getInstance().registerAsync(); + } + + @WorkerThread + @Override + public void handlePush(Intent intent) { + if (intent != null) { + String action = intent.getAction(); + if (REGISTER_RESPONSE_ACTION.equals(action)) { + handleGcmRegistrationIntent(intent); + } else if (RECEIVE_PUSH_ACTION.equals(action)) { + handleGcmPushIntent(intent); + } else { + PLog.e(TAG, "PushService got unknown intent in GCM mode: " + intent); + } + } + } + + @WorkerThread + private void handleGcmRegistrationIntent(Intent intent) { + try { + // Have to block here since we are already in a background thread and as soon as we return, + // PushService may exit. + GcmRegistrar.getInstance().handleRegistrationIntentAsync(intent).waitForCompletion(); + } catch (InterruptedException e) { + // do nothing + } + } + + @WorkerThread + private void handleGcmPushIntent(Intent intent) { + String messageType = intent.getStringExtra("message_type"); + if (messageType != null) { + /* + * The GCM docs reserve the right to use the message_type field for new actions, but haven't + * documented what those new actions are yet. For forwards compatibility, ignore anything + * with a message_type field. + */ + PLog.i(TAG, "Ignored special message type " + messageType + " from GCM via intent " + intent); + } else { + String pushId = intent.getStringExtra("push_id"); + String timestamp = intent.getStringExtra("time"); + String dataString = intent.getStringExtra("data"); + String channel = intent.getStringExtra("channel"); + + JSONObject data = null; + if (dataString != null) { + try { + data = new JSONObject(dataString); + } catch (JSONException e) { + PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e); + return; + } + } + + PushRouter.getInstance().handlePush(pushId, timestamp, channel, data); + } + } + +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmRegistrar.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmRegistrar.java new file mode 100644 index 0000000..596a842 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmRegistrar.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.SystemClock; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * A class that manages registering for GCM and updating the registration if it is out of date, + * used by {@link com.parse.GcmPushHandler}. + */ +/** package */ class GcmRegistrar { + private static final String TAG = "com.parse.GcmRegistrar"; + private static final String REGISTRATION_ID_EXTRA = "registration_id"; + private static final String ERROR_EXTRA = "error"; + + private static final String SENDER_ID_EXTRA = "com.parse.push.gcm_sender_id"; + + public static final String REGISTER_ACTION = "com.google.android.c2dm.intent.REGISTER"; + + private static final String FILENAME_DEVICE_TOKEN_LAST_MODIFIED = "deviceTokenLastModified"; + private long localDeviceTokenLastModified; + private final Object localDeviceTokenLastModifiedMutex = new Object(); + + public static GcmRegistrar getInstance() { + return Singleton.INSTANCE; + } + + private static class Singleton { + public static final GcmRegistrar INSTANCE = new GcmRegistrar(Parse.getApplicationContext()); + } + + private static String actualSenderIDFromExtra(Object senderIDExtra) { + if (!(senderIDExtra instanceof String)) { + return null; + } + + String senderID = (String)senderIDExtra; + if (!senderID.startsWith("id:")) { + return null; + } + + return senderID.substring(3); + } + + private final Object lock = new Object(); + private Request request = null; + private Context context = null; + + // This a package-level constructor for unit testing only. Otherwise, use getInstance(). + /* package */ GcmRegistrar(Context context) { + this.context = context; + } + + /** + * Does nothing if the client already has a valid GCM registration id. Otherwise, sends out a + * GCM registration request and saves the resulting registration id to the server via + * ParseInstallation. + */ + public Task registerAsync() { + if (ManifestInfo.getPushType() != PushType.GCM) { + return Task.forResult(null); + } + synchronized (lock) { + /* + * If we don't yet have a device token, mark this installation as wanting to use GCM by + * setting its pushType to GCM. If the registration does not succeed (because the device + * is offline, for instance), then update() will re-register for a GCM device token at + * next app initialize time. + */ + final ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + // Check whether we need to send registration request, if installation does not + // have device token or local device token is stale, we need to send request. + Task checkTask = installation.getDeviceToken() == null + ? Task.forResult(true) + : isLocalDeviceTokenStaleAsync(); + return checkTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!task.getResult()) { + return Task.forResult(null); + } + if (installation.getPushType() != PushType.GCM) { + installation.setPushType(PushType.GCM); + } + // We do not need to wait sendRegistrationRequestAsync, since this task will finish + // after we get the response from GCM, if we wait for this task, it will block our test. + sendRegistrationRequestAsync(); + return Task.forResult(null); + } + }); + } + } + + private Task sendRegistrationRequestAsync() { + synchronized (lock) { + if (request != null) { + return Task.forResult(null); + } + // Look for an element like this as a child of the element: + // + // + // + // The reason why the "id:" prefix is necessary is because Android treats any metadata value + // that is a string of digits as an integer. So the call to Bundle.getString() will actually + // return null for `android:value="567327206255"`. Additionally, Bundle.getInteger() returns + // a 32-bit integer. For `android:value="567327206255"`, this returns a truncated integer + // because 567327206255 is larger than the largest 32-bit integer. + Bundle metaData = ManifestInfo.getApplicationMetadata(context); + String senderID = null; + + if (metaData != null) { + Object senderIDExtra = metaData.get(SENDER_ID_EXTRA); + + if (senderIDExtra != null) { + senderID = actualSenderIDFromExtra(senderIDExtra); + + if (senderID == null) { + PLog.e(TAG, "Found " + SENDER_ID_EXTRA + " element with value \"" + + senderIDExtra.toString() + "\", but the value is missing the expected \"id:\" " + + "prefix."); + return null; + } + } + } + + if (senderID == null) { + PLog.e(TAG, "You must provide " + SENDER_ID_EXTRA + " in your AndroidManifest.xml\n" + + "Make sure to prefix with the value with id:\n\n" + + "\" />"); + return null; + } + + request = Request.createAndSend(context, senderID); + return request.getTask().continueWith(new Continuation() { + @Override + public Void then(Task task) { + Exception e = task.getError(); + if (e != null) { + PLog.e(TAG, "Got error when trying to register for GCM push", e); + } + + synchronized (lock) { + request = null; + } + + return null; + } + }); + } + } + + /** + * Should be called by a broadcast receiver or service to handle the GCM registration response + * intent (com.google.android.c2dm.intent.REGISTRATION). + */ + Task handleRegistrationIntentAsync(Intent intent) { + List> tasks = new ArrayList<>(); + /* + * We have to parse the response here because GCM may send us a new registration_id + * out-of-band without a request in flight. + */ + String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA); + + if (registrationId != null && registrationId.length() > 0) { + PLog.v(TAG, "Received deviceToken <" + registrationId + "> from GCM."); + + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + // Compare the new deviceToken with the old deviceToken, we only update the + // deviceToken if the new one is different from the old one. This does not follow google + // guide strictly. But we find most of the time if user just update the app, the + // registrationId does not change so there is no need to save it again. + if (!registrationId.equals(installation.getDeviceToken())) { + installation.setPushType(PushType.GCM); + installation.setDeviceToken(registrationId); + tasks.add(installation.saveInBackground()); + } + // We need to update the last modified even the deviceToken is the same. Otherwise when the + // app is opened again, isDeviceTokenStale() will always return false so we will send + // request to GCM every time. + tasks.add(updateLocalDeviceTokenLastModifiedAsync()); + } + synchronized (lock) { + if (request != null) { + request.onReceiveResponseIntent(intent); + } + } + return Task.whenAll(tasks); + } + + // Only used by tests. + /* package */ int getRequestIdentifier() { + synchronized (lock) { + return request != null ? request.identifier : 0; + } + } + + /** package for tests */ Task isLocalDeviceTokenStaleAsync() { + return getLocalDeviceTokenLastModifiedAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + long localDeviceTokenLastModified = task.getResult(); + return Task.forResult(localDeviceTokenLastModified != ManifestInfo.getLastModified()); + } + }); + } + + /** package for tests */ Task updateLocalDeviceTokenLastModifiedAsync() { + return Task.call(new Callable() { + @Override + public Void call() throws Exception { + synchronized (localDeviceTokenLastModifiedMutex) { + localDeviceTokenLastModified = ManifestInfo.getLastModified(); + final String localDeviceTokenLastModifiedStr = + String.valueOf(localDeviceTokenLastModified); + try { + ParseFileUtils.writeStringToFile(getLocalDeviceTokenLastModifiedFile(), + localDeviceTokenLastModifiedStr, "UTF-8"); + } catch (IOException e) { + // do nothing + } + } + return null; + } + }, Task.BACKGROUND_EXECUTOR); + } + + private Task getLocalDeviceTokenLastModifiedAsync() { + return Task.call(new Callable() { + @Override + public Long call() throws Exception { + synchronized (localDeviceTokenLastModifiedMutex) { + if (localDeviceTokenLastModified == 0) { + try { + String localDeviceTokenLastModifiedStr = ParseFileUtils.readFileToString( + getLocalDeviceTokenLastModifiedFile(), "UTF-8"); + localDeviceTokenLastModified = Long.valueOf(localDeviceTokenLastModifiedStr); + } catch (IOException e) { + localDeviceTokenLastModified = 0; + } + } + return localDeviceTokenLastModified; + } + } + }, Task.BACKGROUND_EXECUTOR); + } + + /** package for tests */ static File getLocalDeviceTokenLastModifiedFile() { + File dir = Parse.getParseCacheDir("GCMRegistrar"); + return new File(dir, FILENAME_DEVICE_TOKEN_LAST_MODIFIED); + } + + /** package for tests */ static void deleteLocalDeviceTokenLastModifiedFile() { + ParseFileUtils.deleteQuietly(getLocalDeviceTokenLastModifiedFile()); + } + + /** + * Encapsulates the a GCM registration request-response, potentially using AlarmManager to + * schedule retries if the GCM service is not available. + */ + private static class Request { + private static final String RETRY_ACTION = "com.parse.RetryGcmRegistration"; + private static final int MAX_RETRIES = 5; + private static final int BACKOFF_INTERVAL_MS = 3000; + + final private Context context; + final private String senderId; + final private Random random; + final private int identifier; + final private TaskCompletionSource tcs; + final private PendingIntent appIntent; + final private AtomicInteger tries; + final private PendingIntent retryIntent; + final private BroadcastReceiver retryReceiver; + + public static Request createAndSend(Context context, String senderId) { + Request request = new Request(context, senderId); + request.send(); + + return request; + } + + private Request(Context context, String senderId) { + this.context = context; + this.senderId = senderId; + this.random = new Random(); + this.identifier = this.random.nextInt(); + this.tcs = new TaskCompletionSource<>(); + this.appIntent = PendingIntent.getBroadcast(this.context, identifier, new Intent(), 0); + this.tries = new AtomicInteger(0); + + String packageName = this.context.getPackageName(); + Intent intent = new Intent(RETRY_ACTION).setPackage(packageName); + intent.addCategory(packageName); + intent.putExtra("random", identifier); + this.retryIntent = PendingIntent.getBroadcast(this.context, identifier, intent, 0); + + this.retryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && intent.getIntExtra("random", 0) == identifier) { + send(); + } + } + }; + + IntentFilter filter = new IntentFilter(); + filter.addAction(RETRY_ACTION); + filter.addCategory(packageName); + + context.registerReceiver(this.retryReceiver, filter); + } + + public Task getTask() { + return tcs.getTask(); + } + + private void send() { + Intent intent = new Intent(REGISTER_ACTION); + intent.setPackage("com.google.android.gsf"); + intent.putExtra("sender", senderId); + intent.putExtra("app", appIntent); + + ComponentName name = null; + try { + name = context.startService(intent); + } catch (SecurityException exception) { + // do nothing + } + + if (name == null) { + finish(null, "GSF_PACKAGE_NOT_AVAILABLE"); + } + + tries.incrementAndGet(); + + PLog.v(TAG, "Sending GCM registration intent"); + } + + public void onReceiveResponseIntent(Intent intent) { + String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA); + String error = intent.getStringExtra(ERROR_EXTRA); + + if (registrationId == null && error == null) { + PLog.e(TAG, "Got no registration info in GCM onReceiveResponseIntent"); + return; + } + + // Retry with exponential backoff if GCM isn't available. + if ("SERVICE_NOT_AVAILABLE".equals(error) && tries.get() < MAX_RETRIES) { + AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP; + long delay = (1 << tries.get()) * BACKOFF_INTERVAL_MS + random.nextInt(BACKOFF_INTERVAL_MS); + long start = SystemClock.elapsedRealtime() + delay; + manager.set(alarmType, start, retryIntent); + } else { + finish(registrationId, error); + } + } + + private void finish(String registrationId, String error) { + boolean didSetResult; + + if (registrationId != null) { + didSetResult = tcs.trySetResult(registrationId); + } else { + didSetResult = tcs.trySetError(new Exception("GCM registration error: " + error)); + } + + if (didSetResult) { + appIntent.cancel(); + retryIntent.cancel(); + context.unregisterReceiver(this.retryReceiver); + } + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetCallback.java new file mode 100644 index 0000000..cc32071 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code GetCallback} is used to run code after a {@link ParseQuery} is used to fetch a + * {@link ParseObject} in a background thread. + *

+ * The easiest way to use a {@code GetCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the fetch is complete. + * The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ * For example, this sample code fetches an object of class {@code "MyClass"} and id + * {@code myId}. It calls a different function depending on whether the fetch succeeded or not. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.getInBackground(myId, new GetCallback<ParseObject>() {
+ *   public void done(ParseObject object, ParseException e) {
+ *     if (e == null) {
+ *       objectWasRetrievedSuccessfully(object);
+ *     } else {
+ *       objectRetrievalFailed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface GetCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param object + * The object that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + void done(T object, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataCallback.java new file mode 100644 index 0000000..6f96b6b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code GetDataCallback} is used to run code after a {@link ParseFile} fetches its data on a + * background thread. + *

+ * The easiest way to use a {@code GetDataCallback} is through an anonymous inner class. Override + * the {@code done} function to specify what the callback should do after the fetch is complete. + * The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * file.getDataInBackground(new GetDataCallback() {
+ *   public void done(byte[] data, ParseException e) {
+ *     // ...
+ *   }
+ * });
+ * 
+ */ +public interface GetDataCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param data + * The data that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + void done(byte[] data, ParseException e); +} + diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataStreamCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataStreamCallback.java new file mode 100644 index 0000000..cfdd859 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetDataStreamCallback.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.io.InputStream; + +/** + * A {@code GetDataStreamCallback} is used to run code after a {@link ParseFile} fetches its data on + * a background thread. + *

+ * The easiest way to use a {@code GetDataStreamCallback} is through an anonymous inner class. + * Override the {@code done} function to specify what the callback should do after the fetch is + * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * file.getDataStreamInBackground(new GetDataStreamCallback() {
+ *   public void done(InputSteam input, ParseException e) {
+ *     // ...
+ *   }
+ * });
+ * 
+ */ +public interface GetDataStreamCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param input + * The data that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + void done(InputStream input, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetFileCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetFileCallback.java new file mode 100644 index 0000000..a112713 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GetFileCallback.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.io.File; + +/** + * A {@code GetFileCallback} is used to run code after a {@link ParseFile} fetches its data on + * a background thread. + *

+ * The easiest way to use a {@code GetFileCallback} is through an anonymous inner class. + * Override the {@code done} function to specify what the callback should do after the fetch is + * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ *

+ * file.getFileInBackground(new GetFileCallback() {
+ *   public void done(File file, ParseException e) {
+ *     // ...
+ *   }
+ * });
+ * 
+ */ +public interface GetFileCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param file + * The data that was retrieved, or {@code null} if it did not succeed. + * @param e + * The exception raised by the fetch, or {@code null} if it succeeded. + */ + @Override + void done(File file, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/InstallationId.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/InstallationId.java new file mode 100644 index 0000000..eda7c13 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/InstallationId.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.UUID; + +/** + * Since we cannot save dirty ParseObjects to disk and we must be able to persist UUIDs across + * restarts even if the ParseInstallation is not saved, we use this legacy file still as a + * boostrapping environment as well until the full ParseInstallation is cached to disk. + * + * TODO: Allow dirty objects to be saved to disk. + */ +/* package */ class InstallationId { + + private static final String TAG = "InstallationId"; + + private final Object lock = new Object(); + private final File file; + private String installationId; + + public InstallationId(File file) { + this.file = file; + } + + /** + * Loads the installationId from memory, then tries to loads the legacy installationId from disk + * if it is present, or creates a new random UUID. + */ + public String get() { + synchronized (lock) { + if (installationId == null) { + try { + installationId = ParseFileUtils.readFileToString(file, "UTF-8"); + } catch (FileNotFoundException e) { + PLog.i(TAG, "Couldn't find existing installationId file. Creating one instead."); + } catch (IOException e) { + PLog.e(TAG, "Unexpected exception reading installation id from disk", e); + } + } + + if (installationId == null) { + setInternal(UUID.randomUUID().toString()); + } + } + + return installationId; + } + + /** + * Sets the installationId and persists it to disk. + */ + public void set(String newInstallationId) { + synchronized (lock) { + if (ParseTextUtils.isEmpty(newInstallationId) + || newInstallationId.equals(get())) { + return; + } + setInternal(newInstallationId); + } + } + + private void setInternal(String newInstallationId) { + synchronized (lock) { + try { + ParseFileUtils.writeStringToFile(file, newInstallationId, "UTF-8"); + } catch (IOException e) { + PLog.e(TAG, "Unexpected exception writing installation id to disk", e); + } + + installationId = newInstallationId; + } + } + + /* package for tests */ void clear() { + synchronized (lock) { + installationId = null; + ParseFileUtils.deleteQuietly(file); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/KnownParseObjectDecoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/KnownParseObjectDecoder.java new file mode 100644 index 0000000..80fc50f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/KnownParseObjectDecoder.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Map; + +/** + * A subclass of ParseDecoder which can keep ParseObject that + * has been fetched instead of creating a new instance. + */ +/** package */ class KnownParseObjectDecoder extends ParseDecoder { + private Map fetchedObjects; + + public KnownParseObjectDecoder(Map fetchedObjects) { + super(); + this.fetchedObjects = fetchedObjects; + } + + /** + * If the object has been fetched, the fetched object will be returned. Otherwise a + * new created object will be returned. + */ + @Override + protected ParseObject decodePointer(String className, String objectId) { + if (fetchedObjects != null && fetchedObjects.containsKey(objectId)) { + return fetchedObjects.get(objectId); + } + return super.decodePointer(className, objectId); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Lists.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Lists.java new file mode 100644 index 0000000..cd49cab --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Lists.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 The Guava Authors + * + * 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.parse; + +import java.util.AbstractList; +import java.util.List; + +/** + * Static utility methods pertaining to {@link List} instances. Also see this + * class's counterparts {@link Sets}, {@link Maps} and {@link Queues}. + * + *

See the Guava User Guide article on + * {@code Lists}. + * + * @author Kevin Bourrillion + * @author Mike Bostock + * @author Louis Wasserman + * @since 2.0 + */ +/** package */ class Lists { + + /** + * Returns consecutive sublists of a list, each of the same size (the final list may be smaller). + * For example, partitioning a list containing [a, b, c, d, e] with a partition size of 3 yields + * [[a, b, c], [d, e]] -- an outer list containing two inner lists of three and two elements, all + * in the original order. + * + * The outer list is unmodifiable, but reflects the latest state of the source list. The inner + * lists are sublist views of the original list, produced on demand using List.subList(int, int), + * and are subject to all the usual caveats about modification as explained in that API. + * + * @param list the list to return consecutive sublists of + * @param size the desired size of each sublist (the last may be smaller) + * @return a list of consecutive sublists + */ + /* package */ static List> partition(List list, int size) { + return new Partition<>(list, size); + } + + private static class Partition extends AbstractList> { + + private final List list; + private final int size; + + public Partition(List list, int size) { + this.list = list; + this.size = size; + } + + @Override + public List get(int location) { + int start = location * size; + int end = Math.min(start + size, list.size()); + return list.subList(start, end); + } + + @Override + public int size() { + return (int) Math.ceil((double)list.size() / size); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocalIdManager.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocalIdManager.java new file mode 100644 index 0000000..a744234 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocalIdManager.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.io.File; +import java.io.IOException; +import java.util.Random; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Manages a set of local ids and possible mappings to global Parse objectIds. This class is + * thread-safe. + */ +/** package */ class LocalIdManager { + + /** + * Internal class representing all the information we know about a local id. + */ + private static class MapEntry { + String objectId; + int retainCount; + } + + // Path to the local id storage on disk. + private final File diskPath; + + // Random generator for inventing new ids. + private final Random random; + + /** + * Creates a new LocalIdManager with default options. + */ + /* package for tests */ LocalIdManager(File root) { + diskPath = new File(root, "LocalId"); + random = new Random(); + } + + /** + * Returns true if localId has the right basic format for a local id. + */ + private boolean isLocalId(String localId) { + if (!localId.startsWith("local_")) { + return false; + } + for (int i = 6; i < localId.length(); ++i) { + char c = localId.charAt(i); + if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f')) { + return false; + } + } + return true; + } + + /** + * Grabs one entry in the local id map off the disk. + */ + private synchronized MapEntry getMapEntry(String localId) { + if (!isLocalId(localId)) { + throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); + } + + try { + JSONObject json = ParseFileUtils.readFileToJSONObject(new File(diskPath, localId)); + + MapEntry entry = new MapEntry(); + entry.retainCount = json.optInt("retainCount", 0); + entry.objectId = json.optString("objectId", null); + return entry; + } catch (IOException | JSONException e) { + return new MapEntry(); + } + } + + /** + * Writes one entry to the local id map on disk. + */ + private synchronized void putMapEntry(String localId, MapEntry entry) { + if (!isLocalId(localId)) { + throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); + } + + JSONObject json = new JSONObject(); + try { + json.put("retainCount", entry.retainCount); + if (entry.objectId != null) { + json.put("objectId", entry.objectId); + } + } catch (JSONException je) { + throw new IllegalStateException("Error creating local id map entry.", je); + } + + File file = new File(diskPath, localId); + if (!diskPath.exists()) { + diskPath.mkdirs(); + } + + try { + ParseFileUtils.writeJSONObjectToFile(file, json); + } catch (IOException e) { + //TODO (grantland): We should do something if this fails... + } + } + + /** + * Removes an entry from the local id map on disk. + */ + private synchronized void removeMapEntry(String localId) { + if (!isLocalId(localId)) { + throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); + } + File file = new File(diskPath, localId); + ParseFileUtils.deleteQuietly(file); + } + + /** + * Creates a new local id. + */ + synchronized String createLocalId() { + long localIdNumber = random.nextLong(); + String localId = "local_" + Long.toHexString(localIdNumber); + + if (!isLocalId(localId)) { + throw new IllegalStateException("Generated an invalid local id: \"" + localId + "\". " + + "This should never happen. Open a bug at https://github.com/parse-community/parse-server"); + } + + return localId; + } + + /** + * Increments the retain count of a local id on disk. + */ + synchronized void retainLocalIdOnDisk(String localId) { + MapEntry entry = getMapEntry(localId); + entry.retainCount++; + putMapEntry(localId, entry); + } + + /** + * Decrements the retain count of a local id on disk. If the retain count hits zero, the id is + * forgotten forever. + */ + synchronized void releaseLocalIdOnDisk(String localId) { + MapEntry entry = getMapEntry(localId); + entry.retainCount--; + + if (entry.retainCount > 0) { + putMapEntry(localId, entry); + } else { + removeMapEntry(localId); + } + } + + /** + * Returns the objectId associated with a given local id. Returns null if no objectId is yet known + * for the local id. + */ + synchronized String getObjectId(String localId) { + MapEntry entry = getMapEntry(localId); + return entry.objectId; + } + + /** + * Sets the objectId associated with a given local id. + */ + synchronized void setObjectId(String localId, String objectId) { + MapEntry entry = getMapEntry(localId); + if (entry.retainCount > 0) { + if (entry.objectId != null) { + throw new IllegalStateException( + "Tried to set an objectId for a localId that already has one."); + } + entry.objectId = objectId; + putMapEntry(localId, entry); + } + } + + /** + * Clears all local ids from the map. Returns true is the cache was already empty. + */ + synchronized boolean clear() throws IOException { + String[] files = diskPath.list(); + if (files == null) { + return false; + } + if (files.length == 0) { + return false; + } + for (String fileName : files) { + File file = new File(diskPath, fileName); + if (!file.delete()) { + throw new IOException("Unable to delete file " + fileName + " in localId cache."); + } + } + return true; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationCallback.java new file mode 100644 index 0000000..ef6d1f6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationCallback.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + + +/** + * A {@code LocationCallback} is used to run code after a Location has been fetched by + * {@link com.parse.ParseGeoPoint#getCurrentLocationInBackground(long, android.location.Criteria)}. + *

+ * The easiest way to use a {@code LocationCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the location has been + * fetched. The {@code done} function will be run in the UI thread, while the location check + * happens in a background thread. This ensures that the UI does not freeze while the fetch happens. + *

+ * For example, this sample code defines a timeout for fetching the user's current location, and + * provides a callback. Within the callback, the success and failure cases are handled differently. + *

+ *

+ * ParseGeoPoint.getCurrentLocationAsync(1000, new LocationCallback() {
+ *   public void done(ParseGeoPoint geoPoint, ParseException e) {
+ *     if (e == null) {
+ *       // do something with your new ParseGeoPoint
+ *     } else {
+ *       // handle your error
+ *       e.printStackTrace();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface LocationCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the location fetch is complete. + * + * @param geoPoint + * The {@link ParseGeoPoint} returned by the location fetch. + * @param e + * The exception raised by the location fetch, or {@code null} if it succeeded. + */ + @Override + void done(ParseGeoPoint geoPoint, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationNotifier.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationNotifier.java new file mode 100644 index 0000000..d4661cf --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LocationNotifier.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import bolts.Capture; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * LocationNotifier is a wrapper around fetching the current device's location. It looks for the GPS + * and Network LocationProviders by default (printStackTrace()'ing if, for example, the app doesn't + * have the correct permissions in its AndroidManifest.xml). This class is intended to be used for a + * single location update. + *

+ * When testing, if a fakeLocation is provided (via setFakeLocation()), we don't wait for the + * LocationManager to fire or for the timer to run out; instead, we build a local LocationListener, + * then call the onLocationChanged() method manually. + */ +/** package */ class LocationNotifier { + private static Location fakeLocation = null; + + /** + * Asynchronously gets the current location of the device. + * + * This will request location updates from the best provider that match the given criteria + * and return the first location received. + * + * You can customize the criteria to meet your specific needs. + * * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer + * times for a fix. + * * For better battery efficiency and faster location fixes, you can set + * {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy. + * + * @param context + * The context used to request location updates. + * @param timeout + * The number of milliseconds to allow before timing out. + * @param criteria + * The application criteria for selecting a location provider. + * + * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) + * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) + */ + /* package */ static Task getCurrentLocationAsync(Context context, + long timeout, Criteria criteria) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + final Capture> timeoutFuture = new Capture<>(); + final LocationManager manager = + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + final LocationListener listener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + if (location == null) { + return; + } + + timeoutFuture.get().cancel(true); + + tcs.trySetResult(location); + manager.removeUpdates(this); + } + + @Override + public void onProviderDisabled(String provider) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }; + + timeoutFuture.set(ParseExecutors.scheduled().schedule(new Runnable() { + @Override + public void run() { + tcs.trySetError(new ParseException(ParseException.TIMEOUT, "Location fetch timed out.")); + manager.removeUpdates(listener); + } + }, timeout, TimeUnit.MILLISECONDS)); + + String provider = manager.getBestProvider(criteria, true); + if (provider != null) { + manager.requestLocationUpdates(provider, /* minTime */ 0, /* minDistance */ 0.0f, listener); + } + + if (fakeLocation != null) { + listener.onLocationChanged(fakeLocation); + } + + return tcs.getTask(); + } + + /** + * Helper method for testing. + */ + /* package */ static void setFakeLocation(Location location) { + fakeLocation = location; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LockSet.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LockSet.java new file mode 100644 index 0000000..593990a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LockSet.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.locks.Lock; + +/** package */ class LockSet { + private static final WeakHashMap stableIds = new WeakHashMap<>(); + private static long nextStableId = 0L; + + private final Set locks; + + public LockSet(Collection locks) { + this.locks = new TreeSet<>(new Comparator() { + @Override + public int compare(Lock lhs, Lock rhs) { + Long lhsId = getStableId(lhs); + Long rhsId = getStableId(rhs); + return lhsId.compareTo(rhsId); + } + }); + this.locks.addAll(locks); + } + + private static Long getStableId(Lock lock) { + synchronized (stableIds) { + if (stableIds.containsKey(lock)) { + return stableIds.get(lock); + } + long id = nextStableId++; + stableIds.put(lock, id); + return id; + } + } + + public void lock() { + for (Lock l : locks) { + l.lock(); + } + } + + public void unlock() { + for (Lock l : locks) { + l.unlock(); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogInCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogInCallback.java new file mode 100644 index 0000000..9c95df1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogInCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code LogInCallback} is used to run code after logging in a user. + *

+ * The easiest way to use a {@code LogInCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the login is complete. + * The {@code done} function will be run in the UI thread, while the login happens in a + * background thread. This ensures that the UI does not freeze while the save happens. + *

+ * For example, this sample code logs in a user and calls a different function depending on whether + * the login succeeded or not. + *

+ *

+ * ParseUser.logInInBackground("username", "password", new LogInCallback() {
+ *   public void done(ParseUser user, ParseException e) {
+ *     if (e == null && user != null) {
+ *       loginSuccessful();
+ *     } else if (user == null) {
+ *       usernameOrPasswordIsInvalid();
+ *     } else {
+ *       somethingWentWrong();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface LogInCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param user + * The user that logged in, if the username and password is valid. + * @param e + * The exception raised by the login, or {@code null} if it succeeded. + */ + @Override + void done(ParseUser user, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogOutCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogOutCallback.java new file mode 100644 index 0000000..7c7a8f7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/LogOutCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code LogOutCallback} is used to run code after logging out a user. + *

+ * The easiest way to use a {@code LogOutCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the login is complete. + * The {@code done} function will be run in the UI thread, while the login happens in a + * background thread. This ensures that the UI does not freeze while the save happens. + *

+ * For example, this sample code logs out a user and calls a different function depending on whether + * the log out succeeded or not. + *

+ *

+ * ParseUser.logOutInBackground(new LogOutCallback() {
+ *   public void done(ParseException e) {
+ *     if (e == null) {
+ *       logOutSuccessful();
+ *     } else {
+ *       somethingWentWrong();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface LogOutCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param e + * The exception raised by the log out, or {@code null} if it succeeded. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ManifestInfo.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ManifestInfo.java new file mode 100644 index 0000000..a7f1c73 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ManifestInfo.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Bundle; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A utility class for retrieving app metadata such as the app name, default icon, whether or not + * the app declares the correct permissions for push, etc. + */ +/** package */ class ManifestInfo { + private static final String TAG = "com.parse.ManifestInfo"; + + private static final Object lock = new Object(); + private static long lastModified = -1; + /* package */ static int versionCode = -1; + /* package */ static String versionName = null; + private static int iconId = 0; + private static String displayName = null; + private static PushType pushType; + + /** + * Returns the last time this application's APK was modified on disk. This is a proxy for both + * version changes and if the APK has been restored from backup onto a different device. + */ + public static long getLastModified() { + synchronized (lock) { + if (lastModified == -1) { + File apkPath = new File(getContext().getApplicationInfo().sourceDir); + lastModified = apkPath.lastModified(); + } + } + + return lastModified; + } + + /** + * Returns the version code for this app, as specified by the android:versionCode attribute in the + * element of the manifest. + */ + public static int getVersionCode() { + synchronized (lock) { + if (versionCode == -1) { + try { + versionCode = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionCode; + } catch (NameNotFoundException e) { + PLog.e(TAG, "Couldn't find info about own package", e); + } + } + } + + return versionCode; + } + + /** + * Returns the version name for this app, as specified by the android:versionName attribute in the + * element of the manifest. + */ + public static String getVersionName() { + synchronized (lock) { + if (versionName == null) { + try { + versionName = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionName; + } catch (NameNotFoundException e) { + PLog.e(TAG, "Couldn't find info about own package", e); + } + } + } + + return versionName; + } + + /** + * Returns the display name of the app used by the app launcher, as specified by the android:label + * attribute in the element of the manifest. + */ + public static String getDisplayName(Context context) { + synchronized (lock) { + if (displayName == null) { + ApplicationInfo appInfo = context.getApplicationInfo(); + displayName = context.getPackageManager().getApplicationLabel(appInfo).toString(); + } + } + + return displayName; + } + + /** + * Returns the default icon id used by this application, as specified by the android:icon + * attribute in the element of the manifest. + */ + public static int getIconId() { + synchronized (lock) { + if (iconId == 0) { + iconId = getContext().getApplicationInfo().icon; + } + } + return iconId; + } + + /** + * Returns whether the given action has an associated receiver defined in the manifest. + */ + /* package */ static boolean hasIntentReceiver(String action) { + return !getIntentReceivers(action).isEmpty(); + } + + /** + * Returns a list of ResolveInfo objects corresponding to the BroadcastReceivers with Intent Filters + * specifying the given action within the app's package. + */ + /* package */ static List getIntentReceivers(String... actions) { + Context context = getContext(); + PackageManager pm = context.getPackageManager(); + String packageName = context.getPackageName(); + List list = new ArrayList<>(); + + for (String action : actions) { + list.addAll(pm.queryBroadcastReceivers( + new Intent(action), + PackageManager.GET_INTENT_FILTERS)); + } + + for (int i = list.size() - 1; i >= 0; --i) { + String receiverPackageName = list.get(i).activityInfo.packageName; + if (!receiverPackageName.equals(packageName)) { + list.remove(i); + } + } + return list; + } + + // Should only be used for tests. + static void setPushType(PushType newPushType) { + synchronized (lock) { + pushType = newPushType; + } + } + + /** + * Inspects the app's manifest and returns whether the manifest contains required declarations to + * be able to use GCM for push. + */ + public static PushType getPushType() { + synchronized (lock) { + if (pushType == null) { + pushType = findPushType(); + PLog.v(TAG, "Using " + pushType + " for push."); + } + } + return pushType; + } + + + private static PushType findPushType() { + if (!ParsePushBroadcastReceiver.isSupported()) { + return PushType.NONE; + } + + if (!PushServiceUtils.isSupported()) { + return PushType.NONE; + } + + // Ordered by preference. + PushType[] types = PushType.types(); + for (PushType type : types) { + PushHandler handler = PushHandler.Factory.create(type); + PushHandler.SupportLevel level = handler.isSupported(); + String message = handler.getWarningMessage(level); + switch (level) { + case MISSING_REQUIRED_DECLARATIONS: // Can't use. notify. + if (message != null) PLog.e(TAG, message); + break; + case MISSING_OPTIONAL_DECLARATIONS: // Using anyway. + if (message != null) PLog.w(TAG, message); + return type; + case SUPPORTED: + return type; + } + } + return PushType.NONE; + } + + /* + * Returns a message that can be written to the system log if an app expects push to be enabled, + * but push isn't actually enabled because the manifest is misconfigured. + */ + static String getPushDisabledMessage() { + return "Push is not configured for this app because the app manifest is missing required " + + "declarations. To configure GCM, please add the following declarations to your app manifest: " + + GcmPushHandler.getWarningMessage(); + } + + + private static Context getContext() { + return Parse.getApplicationContext(); + } + + private static PackageManager getPackageManager() { + return getContext().getPackageManager(); + } + + private static ApplicationInfo getApplicationInfo(Context context, int flags) { + try { + return context.getPackageManager().getApplicationInfo(context.getPackageName(), flags); + } catch (NameNotFoundException e) { + return null; + } + } + + /** + * @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise null. + */ + public static Bundle getApplicationMetadata(Context context) { + ApplicationInfo info = getApplicationInfo(context, PackageManager.GET_META_DATA); + if (info != null) { + return info.metaData; + } + return null; + } + + private static PackageInfo getPackageInfo(String name) { + PackageInfo info = null; + + try { + info = getPackageManager().getPackageInfo(name, 0); + } catch (NameNotFoundException e) { + // do nothing + } + + return info; + } + + static ServiceInfo getServiceInfo(Class clazz) { + ServiceInfo info = null; + try { + info = getPackageManager().getServiceInfo(new ComponentName(getContext(), clazz), 0); + } catch (NameNotFoundException e) { + // do nothing + } + + return info; + } + + private static ActivityInfo getReceiverInfo(Class clazz) { + ActivityInfo info = null; + try { + info = getPackageManager().getReceiverInfo(new ComponentName(getContext(), clazz), 0); + } catch (NameNotFoundException e) { + // do nothing + } + + return info; + } + + /** + * Returns {@code true} if this package has requested all of the listed permissions. + *

+ * Note: This package might have requested all the permissions, but may not + * be granted all of them. + */ + static boolean hasRequestedPermissions(Context context, String... permissions) { + String packageName = context.getPackageName(); + try { + PackageInfo pi = context.getPackageManager().getPackageInfo( + packageName, PackageManager.GET_PERMISSIONS); + if (pi.requestedPermissions == null) { + return false; + } + return Arrays.asList(pi.requestedPermissions).containsAll(Arrays.asList(permissions)); + } catch (NameNotFoundException e) { + PLog.e(TAG, "Couldn't find info about own package", e); + return false; + } + } + + /** + * Returns {@code true} if this package has been granted all of the listed permissions. + *

+ * Note: This package might have requested all the permissions, but may not + * be granted all of them. + */ + static boolean hasGrantedPermissions(Context context, String... permissions) { + String packageName = context.getPackageName(); + PackageManager packageManager = context.getPackageManager(); + for (String permission : permissions) { + if (packageManager.checkPermission(permission, packageName) != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + + return true; + } + + private static boolean checkResolveInfo(Class clazz, List infoList, String permission) { + for (ResolveInfo info : infoList) { + if (info.activityInfo != null) { + final Class resolveInfoClass; + try { + resolveInfoClass = Class.forName(info.activityInfo.name); + } catch (ClassNotFoundException e) { + break; + } + if (clazz.isAssignableFrom(resolveInfoClass) && (permission == null || permission.equals(info.activityInfo.permission))) { + return true; + } + } + } + + return false; + } + + static boolean checkReceiver(Class clazz, String permission, Intent[] intents) { + for (Intent intent : intents) { + List receivers = getPackageManager().queryBroadcastReceivers(intent, 0); + if (receivers.isEmpty()) { + return false; + } + + if (!checkResolveInfo(clazz, receivers, permission)) { + return false; + } + } + + return true; + } + + static boolean isGooglePlayServicesAvailable() { + return getPackageInfo("com.google.android.gsf") != null; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkObjectController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkObjectController.java new file mode 100644 index 0000000..6ee241f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkObjectController.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class NetworkObjectController implements ParseObjectController { + + private ParseHttpClient client; + private ParseObjectCoder coder; + + public NetworkObjectController(ParseHttpClient client) { + this.client = client; + this.coder = ParseObjectCoder.get(); + } + + @Override + public Task fetchAsync( + final ParseObject.State state, String sessionToken, final ParseDecoder decoder) { + final ParseRESTCommand command = ParseRESTObjectCommand.getObjectCommand( + state.objectId(), + state.className(), + sessionToken); + + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseObject.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + // Copy and clear to create an new empty instance of the same type as `state` + ParseObject.State.Init builder = state.newBuilder().clear(); + return coder.decode(builder, result, decoder) + .isComplete(true) + .build(); + } + }); + } + + @Override + public Task saveAsync( + final ParseObject.State state, + final ParseOperationSet operations, + String sessionToken, + final ParseDecoder decoder) { + /* + * Get the JSON representation of the object, and use some of the information to construct the + * command. + */ + JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); + + ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand( + state, + objectJSON, + sessionToken); + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseObject.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + // Copy and clear to create an new empty instance of the same type as `state` + ParseObject.State.Init builder = state.newBuilder().clear(); + return coder.decode(builder, result, decoder) + .isComplete(false) + .build(); + } + }); + } + + @Override + public List> saveAllAsync( + List states, + List operationsList, + String sessionToken, + List decoders) { + int batchSize = states.size(); + + List commands = new ArrayList<>(batchSize); + ParseEncoder encoder = PointerEncoder.get(); + for (int i = 0; i < batchSize; i++) { + ParseObject.State state = states.get(i); + ParseOperationSet operations = operationsList.get(i); + JSONObject objectJSON = coder.encode(state, operations, encoder); + + ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand( + state, objectJSON, sessionToken); + commands.add(command); + } + + final List> batchTasks = + ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken); + + final List> tasks = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + final ParseObject.State state = states.get(i); + final ParseDecoder decoder = decoders.get(i); + tasks.add(batchTasks.get(i).onSuccess(new Continuation() { + @Override + public ParseObject.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + // Copy and clear to create an new empty instance of the same type as `state` + ParseObject.State.Init builder = state.newBuilder().clear(); + return coder.decode(builder, result, decoder) + .isComplete(false) + .build(); + } + })); + } + return tasks; + } + + @Override + public Task deleteAsync(ParseObject.State state, String sessionToken) { + ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand( + state, sessionToken); + + return command.executeAsync(client).makeVoid(); + } + + @Override + public List> deleteAllAsync( + List states, String sessionToken) { + int batchSize = states.size(); + + List commands = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + ParseObject.State state = states.get(i); + ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand( + state, sessionToken); + commands.add(command); + } + + final List> batchTasks = + ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken); + + List> tasks = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + tasks.add(batchTasks.get(i).makeVoid()); + } + return tasks; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkQueryController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkQueryController.java new file mode 100644 index 0000000..14a71a8 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkQueryController.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class NetworkQueryController extends AbstractQueryController { + + private static final String TAG = "NetworkQueryController"; + + private final ParseHttpClient restClient; + + public NetworkQueryController(ParseHttpClient restClient) { + this.restClient = restClient; + } + + @Override + public Task> findAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + String sessionToken = user != null ? user.getSessionToken() : null; + return findAsync(state, sessionToken, cancellationToken); + } + + @Override + public Task countAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + String sessionToken = user != null ? user.getSessionToken() : null; + return countAsync(state, sessionToken, cancellationToken); + } + + /** + * Retrieves a list of {@link ParseObject}s that satisfy this query from the source. + * + * @return A list of all {@link ParseObject}s obeying the conditions set in this query. + */ + /* package */ Task> findAsync( + final ParseQuery.State state, + String sessionToken, + Task ct) { + final long queryStart = System.nanoTime(); + + final ParseRESTCommand command = ParseRESTQueryCommand.findCommand(state, sessionToken); + + final long querySent = System.nanoTime(); + return command.executeAsync(restClient, ct).onSuccess(new Continuation>() { + @Override + public List then(Task task) throws Exception { + JSONObject json = task.getResult(); + // Cache the results, unless we are ignoring the cache + ParseQuery.CachePolicy policy = state.cachePolicy(); + if (policy != null && (policy != ParseQuery.CachePolicy.IGNORE_CACHE)) { + ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), json.toString()); + } + + long queryReceived = System.nanoTime(); + + List response = convertFindResponse(state, task.getResult()); + + long objectsParsed = System.nanoTime(); + + if (json.has("trace")) { + Object serverTrace = json.get("trace"); + PLog.d("ParseQuery", + String.format("Query pre-processing took %f seconds\n" + + "%s\n" + + "Client side parsing took %f seconds\n", + (querySent - queryStart) / (1000.0f * 1000.0f), + serverTrace, + (objectsParsed - queryReceived) / (1000.0f * 1000.0f))); + } + return response; + } + }, Task.BACKGROUND_EXECUTOR); + } + + /* package */ Task countAsync( + final ParseQuery.State state, + String sessionToken, + Task ct) { + final ParseRESTCommand command = ParseRESTQueryCommand.countCommand(state, sessionToken); + + return command.executeAsync(restClient, ct).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Cache the results, unless we are ignoring the cache + ParseQuery.CachePolicy policy = state.cachePolicy(); + if (policy != null && policy != ParseQuery.CachePolicy.IGNORE_CACHE) { + JSONObject result = task.getResult(); + ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), result.toString()); + } + return task; + } + }, Task.BACKGROUND_EXECUTOR).onSuccess(new Continuation() { + @Override + public Integer then(Task task) throws Exception { + // Convert response + return task.getResult().optInt("count"); + } + }); + } + + // Converts the JSONArray that represents the results of a find command to an + // ArrayList. + /* package */ List convertFindResponse(ParseQuery.State state, + JSONObject response) throws JSONException { + ArrayList answer = new ArrayList<>(); + JSONArray results = response.getJSONArray("results"); + if (results == null) { + PLog.d(TAG, "null results in find response"); + } else { + String resultClassName = response.optString("className", null); + if (resultClassName == null) { + resultClassName = state.className(); + } + for (int i = 0; i < results.length(); ++i) { + JSONObject data = results.getJSONObject(i); + T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys()); + answer.add(object); + + /* + * If there was a $relatedTo constraint on the query, then add any results to the list of + * known objects in the relation for offline caching + */ + ParseQuery.RelationConstraint relation = + (ParseQuery.RelationConstraint) state.constraints().get("$relatedTo"); + if (relation != null) { + relation.getRelation().addKnownObject(object); + } + } + } + + return answer; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkSessionController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkSessionController.java new file mode 100644 index 0000000..4c6cc9e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkSessionController.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class NetworkSessionController implements ParseSessionController { + + private final ParseHttpClient client; + private final ParseObjectCoder coder; + + public NetworkSessionController(ParseHttpClient client) { + this.client = client; + this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject + } + + @Override + public Task getSessionAsync(String sessionToken) { + ParseRESTSessionCommand command = + ParseRESTSessionCommand.getCurrentSessionCommand(sessionToken); + + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseObject.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get()) + .isComplete(true) + .build(); + } + }); + } + + @Override + public Task revokeAsync(String sessionToken) { + return ParseRESTSessionCommand.revoke(sessionToken) + .executeAsync(client) + .makeVoid(); + } + + @Override + public Task upgradeToRevocable(String sessionToken) { + ParseRESTSessionCommand command = + ParseRESTSessionCommand.upgradeToRevocableSessionCommand(sessionToken); + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseObject.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get()) + .isComplete(true) + .build(); + } + }); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkUserController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkUserController.java new file mode 100644 index 0000000..bb3618b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NetworkUserController.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +import java.util.Map; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class NetworkUserController implements ParseUserController { + + private static final int STATUS_CODE_CREATED = 201; + + private final ParseHttpClient client; + private final ParseObjectCoder coder; + private final boolean revocableSession; + + public NetworkUserController(ParseHttpClient client) { + this(client, false); + } + + public NetworkUserController(ParseHttpClient client, boolean revocableSession) { + this.client = client; + this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject + this.revocableSession = revocableSession; + } + + @Override + public Task signUpAsync( + final ParseObject.State state, + ParseOperationSet operations, + String sessionToken) { + JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); + ParseRESTCommand command = ParseRESTUserCommand.signUpUserCommand( + objectJSON, sessionToken, revocableSession); + + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) + .isComplete(false) + .isNew(true) + .build(); + } + }); + } + + //region logInAsync + + @Override + public Task logInAsync( + String username, String password) { + ParseRESTCommand command = ParseRESTUserCommand.logInUserCommand( + username, password, revocableSession); + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + + return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) + .isComplete(true) + .build(); + } + }); + } + + @Override + public Task logInAsync( + ParseUser.State state, ParseOperationSet operations) { + JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); + final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand( + objectJSON, state.sessionToken(), revocableSession); + + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + + // TODO(grantland): Does the server really respond back with complete object data if the + // object isn't new? + boolean isNew = command.getStatusCode() == STATUS_CODE_CREATED; + boolean isComplete = !isNew; + + return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) + .isComplete(isComplete) + .isNew(isNew) + .build(); + } + }); + } + + @Override + public Task logInAsync( + final String authType, final Map authData) { + final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand( + authType, authData, revocableSession); + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + + return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) + .isComplete(true) + .isNew(command.getStatusCode() == STATUS_CODE_CREATED) + .putAuthData(authType, authData) + .build(); + } + }); + } + + //endregion + + @Override + public Task getUserAsync(String sessionToken) { + ParseRESTCommand command = ParseRESTUserCommand.getCurrentUserCommand(sessionToken); + return command.executeAsync(client).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + + return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) + .isComplete(true) + .build(); + } + }); + } + + @Override + public Task requestPasswordResetAsync(String email) { + ParseRESTCommand command = ParseRESTUserCommand.resetPasswordResetCommand(email); + return command.executeAsync(client).makeVoid(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NoObjectsEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NoObjectsEncoder.java new file mode 100644 index 0000000..5780e30 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NoObjectsEncoder.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +/** + * Throws an exception if someone attemps to encode a {@code ParseObject}. + */ +/** package */ class NoObjectsEncoder extends ParseEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final NoObjectsEncoder INSTANCE = new NoObjectsEncoder(); + public static NoObjectsEncoder get() { + return INSTANCE; + } + + @Override + public JSONObject encodeRelatedObject(ParseObject object) { + throw new IllegalArgumentException("ParseObjects not allowed here"); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NotificationCompat.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NotificationCompat.java new file mode 100644 index 0000000..bc092d7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/NotificationCompat.java @@ -0,0 +1,440 @@ +/* + * 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.parse; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.widget.RemoteViews; + +/** + * A simple implementation of the NotificationCompat class from android-support-v4 + * It only differentiates between devices before and after JellyBean because the only extra + * feature that we currently support between the two device types is BigTextStyle notifications. + * This class takes advantage of lazy class loading to eliminate warnings of the type + * 'Could not find class...' + */ +/** package */ class NotificationCompat { + /** + * Obsolete flag indicating high-priority notifications; use the priority field instead. + * + * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value. + */ + public static final int FLAG_HIGH_PRIORITY = 0x00000080; + + /** + * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}. + * If your application does not prioritize its own notifications, + * use this value for all notifications. + */ + public static final int PRIORITY_DEFAULT = 0; + + private static final NotificationCompatImpl IMPL; + + interface NotificationCompatImpl { + Notification build(Builder b); + } + + static class NotificationCompatImplBase implements NotificationCompatImpl { + @Override + public Notification build(Builder builder) { + Notification result = builder.mNotification; + NotificationCompat.Builder newBuilder = new NotificationCompat.Builder(builder.mContext); + newBuilder.setContentTitle(builder.mContentTitle); + newBuilder.setContentText(builder.mContentText); + newBuilder.setContentIntent(builder.mContentIntent); + // translate high priority requests into legacy flag + if (builder.mPriority > PRIORITY_DEFAULT) { + result.flags |= FLAG_HIGH_PRIORITY; + } + return newBuilder.build(); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + static class NotificationCompatPostJellyBean implements NotificationCompatImpl { + private Notification.Builder postJellyBeanBuilder; + @Override + public Notification build(Builder b) { + postJellyBeanBuilder = new Notification.Builder(b.mContext); + postJellyBeanBuilder.setContentTitle(b.mContentTitle) + .setContentText(b.mContentText) + .setTicker(b.mNotification.tickerText) + .setSmallIcon(b.mNotification.icon, b.mNotification.iconLevel) + .setContentIntent(b.mContentIntent) + .setDeleteIntent(b.mNotification.deleteIntent) + .setAutoCancel((b.mNotification.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setLargeIcon(b.mLargeIcon) + .setDefaults(b.mNotification.defaults); + if (b.mStyle != null) { + if (b.mStyle instanceof Builder.BigTextStyle) { + Builder.BigTextStyle staticStyle = (Builder.BigTextStyle) b.mStyle; + Notification.BigTextStyle style = new Notification.BigTextStyle(postJellyBeanBuilder) + .setBigContentTitle(staticStyle.mBigContentTitle) + .bigText(staticStyle.mBigText); + if (staticStyle.mSummaryTextSet) { + style.setSummaryText(staticStyle.mSummaryText); + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + postJellyBeanBuilder.setChannelId(b.mNotificationChannelId); + } + return postJellyBeanBuilder.build(); + } + } + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + IMPL = new NotificationCompatPostJellyBean(); + } else { + IMPL = new NotificationCompatImplBase(); + } + } + + public static class Builder { + /** + * Maximum length of CharSequences accepted by Builder and friends. + * + *

+ * Avoids spamming the system with overly large strings such as full e-mails. + */ + private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; + + Context mContext; + + CharSequence mContentTitle; + CharSequence mContentText; + PendingIntent mContentIntent; + Bitmap mLargeIcon; + int mPriority; + Style mStyle; + String mNotificationChannelId; + + Notification mNotification = new Notification(); + + /** + * Constructor. + * + * Automatically sets the when field to {@link System#currentTimeMillis() + * System.currentTimeMillis()} and the audio stream to the + * {@link Notification#STREAM_DEFAULT}. + * + * @param context A {@link Context} that will be used to construct the + * RemoteViews. The Context will not be held past the lifetime of this + * Builder object. + */ + public Builder(Context context) { + mContext = context; + + // Set defaults to match the defaults of a Notification + mNotification.when = System.currentTimeMillis(); + mNotification.audioStreamType = Notification.STREAM_DEFAULT; + mPriority = PRIORITY_DEFAULT; + } + + public Builder setWhen(long when) { + mNotification.when = when; + return this; + } + + /** + * Set the small icon to use in the notification layouts. Different classes of devices + * may return different sizes. See the UX guidelines for more information on how to + * design these icons. + * + * @param icon A resource ID in the application's package of the drawble to use. + */ + public Builder setSmallIcon(int icon) { + mNotification.icon = icon; + return this; + } + + /** + * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional + * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable + * LevelListDrawable}. + * + * @param icon A resource ID in the application's package of the drawble to use. + * @param level The level to use for the icon. + * + * @see android.graphics.drawable.LevelListDrawable + */ + public Builder setSmallIcon(int icon, int level) { + mNotification.icon = icon; + mNotification.iconLevel = level; + return this; + } + + /** + * Set the title (first row) of the notification, in a standard notification. + */ + public Builder setContentTitle(CharSequence title) { + mContentTitle = limitCharSequenceLength(title); + return this; + } + + /** + * Set the notification channel of the notification, in a standard notification. + */ + public Builder setNotificationChannel(String notificationChannelId) { + mNotificationChannelId = notificationChannelId; + return this; + } + + /** + * Set the text (second row) of the notification, in a standard notification. + */ + public Builder setContentText(CharSequence text) { + mContentText = limitCharSequenceLength(text); + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is clicked. + * If you do not supply an intent, you can now add PendingIntents to individual + * views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent + * RemoteViews.setOnClickPendingIntent(int,PendingIntent)}. Be sure to + * read {@link Notification#contentIntent Notification.contentIntent} for + * how to correctly use this. + */ + public Builder setContentIntent(PendingIntent intent) { + mContentIntent = intent; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is cleared by the user + * directly from the notification panel. For example, this intent is sent when the user + * clicks the "Clear all" button, or the individual "X" buttons on notifications. This + * intent is not sent when the application calls {@link NotificationManager#cancel + * NotificationManager.cancel(int)}. + */ + public Builder setDeleteIntent(PendingIntent intent) { + mNotification.deleteIntent = intent; + return this; + } + + /** + * Set the text that is displayed in the status bar when the notification first + * arrives. + */ + public Builder setTicker(CharSequence tickerText) { + mNotification.tickerText = limitCharSequenceLength(tickerText); + return this; + } + + /** + * Set the large icon that is shown in the ticker and notification. + */ + public Builder setLargeIcon(Bitmap icon) { + mLargeIcon = icon; + return this; + } + + /** + * Setting this flag will make it so the notification is automatically + * canceled when the user clicks it in the panel. The PendingIntent + * set with {@link #setDeleteIntent} will be broadcast when the notification + * is canceled. + */ + public Builder setAutoCancel(boolean autoCancel) { + setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel); + return this; + } + + /** + * Set the default notification options that will be used. + *

+ * The value should be one or more of the following fields combined with + * bitwise-or: + * {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE}, + * {@link Notification#DEFAULT_LIGHTS}. + *

+ * For all default values, use {@link Notification#DEFAULT_ALL}. + */ + public Builder setDefaults(int defaults) { + mNotification.defaults = defaults; + if ((defaults & Notification.DEFAULT_LIGHTS) != 0) { + mNotification.flags |= Notification.FLAG_SHOW_LIGHTS; + } + return this; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mNotification.flags |= mask; + } else { + mNotification.flags &= ~mask; + } + } + + /** + * Set the relative priority for this notification. + * + * Priority is an indication of how much of the user's + * valuable attention should be consumed by this + * notification. Low-priority notifications may be hidden from + * the user in certain situations, while the user might be + * interrupted for a higher-priority notification. + * The system sets a notification's priority based on various factors including the + * setPriority value. The effect may differ slightly on different platforms. + */ + public Builder setPriority(int pri) { + mPriority = pri; + return this; + } + + /** + * Add a rich notification style to be applied at build time. + *
+ * If the platform does not provide rich notification styles, this method has no effect. The + * user will always see the normal notification style. + * + * @param style Object responsible for modifying the notification style. + */ + public Builder setStyle(Style style) { + if (mStyle != style) { + mStyle = style; + if (mStyle != null) { + mStyle.setBuilder(this); + } + } + return this; + } + + /** + * @deprecated Use {@link #build()} instead. + */ + @Deprecated + public Notification getNotification() { + return IMPL.build(this); + } + + /** + * Combine all of the options that have been set and return a new {@link Notification} + * object. + */ + public Notification build() { + return IMPL.build(this); + } + + protected static CharSequence limitCharSequenceLength(CharSequence cs) { + if (cs == null) return cs; + if (cs.length() > MAX_CHARSEQUENCE_LENGTH) { + cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH); + } + return cs; + } + + /** + * An object that can apply a rich notification style to a {@link Notification.Builder} + * object. + *
+ * If the platform does not provide rich notification styles, methods in this class have no + * effect. + */ + public static abstract class Style + { + Builder mBuilder; + CharSequence mBigContentTitle; + CharSequence mSummaryText; + boolean mSummaryTextSet = false; + + public void setBuilder(Builder builder) { + if (mBuilder != builder) { + mBuilder = builder; + if (mBuilder != null) { + mBuilder.setStyle(this); + } + } + } + + public Notification build() { + Notification notification = null; + if (mBuilder != null) { + notification = mBuilder.build(); + } + return notification; + } + } + + /** + * Helper class for generating large-format notifications that include a lot of text. + * + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *

+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle("New mail from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigTextStyle()
+     *         .bigText(aVeryLongString))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class BigTextStyle extends Style { + CharSequence mBigText; + + public BigTextStyle() { + } + + public BigTextStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public BigTextStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = title; + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public BigTextStyle setSummaryText(CharSequence cs) { + mSummaryText = cs; + mSummaryTextSet = true; + return this; + } + + /** + * Provide the longer text to be displayed in the big form of the + * template in place of the content text. + */ + public BigTextStyle bigText(CharSequence cs) { + mBigText = cs; + return this; + } + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Numbers.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Numbers.java new file mode 100644 index 0000000..3949267 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Numbers.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * Static utility methods pertaining to {@link Number} instances. + */ +/** package */ class Numbers { + + /** + * Add two {@link Number} instances. + */ + /* package */ static Number add(Number first, Number second) { + if (first instanceof Double || second instanceof Double) { + return first.doubleValue() + second.doubleValue(); + } else if (first instanceof Float || second instanceof Float) { + return first.floatValue() + second.floatValue(); + } else if (first instanceof Long || second instanceof Long) { + return first.longValue() + second.longValue(); + } else if (first instanceof Integer || second instanceof Integer) { + return first.intValue() + second.intValue(); + } else if (first instanceof Short || second instanceof Short) { + return first.shortValue() + second.shortValue(); + } else if (first instanceof Byte || second instanceof Byte) { + return first.byteValue() + second.byteValue(); + } else { + throw new RuntimeException("Unknown number type."); + } + } + + /** + * Subtract two {@link Number} instances. + */ + /* package */ static Number subtract(Number first, Number second) { + if (first instanceof Double || second instanceof Double) { + return first.doubleValue() - second.doubleValue(); + } else if (first instanceof Float || second instanceof Float) { + return first.floatValue() - second.floatValue(); + } else if (first instanceof Long || second instanceof Long) { + return first.longValue() - second.longValue(); + } else if (first instanceof Integer || second instanceof Integer) { + return first.intValue() - second.intValue(); + } else if (first instanceof Short || second instanceof Short) { + return first.shortValue() - second.shortValue(); + } else if (first instanceof Byte || second instanceof Byte) { + return first.byteValue() - second.byteValue(); + } else { + throw new RuntimeException("Unknown number type."); + } + } + + /** + * Compare two {@link Number} instances. + */ + /* package */ static int compare(Number first, Number second) { + if (first instanceof Double || second instanceof Double) { + return (int) Math.signum(first.doubleValue() - second.doubleValue()); + } else if (first instanceof Float || second instanceof Float) { + return (int) Math.signum(first.floatValue() - second.floatValue()); + } else if (first instanceof Long || second instanceof Long) { + long diff = first.longValue() - second.longValue(); + return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0); + } else if (first instanceof Integer || second instanceof Integer) { + return first.intValue() - second.intValue(); + } else if (first instanceof Short || second instanceof Short) { + return first.shortValue() - second.shortValue(); + } else if (first instanceof Byte || second instanceof Byte) { + return first.byteValue() - second.byteValue(); + } else { + throw new RuntimeException("Unknown number type."); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineObjectStore.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineObjectStore.java new file mode 100644 index 0000000..526d6fb --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineObjectStore.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Arrays; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class OfflineObjectStore implements ParseObjectStore { + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + private static Task migrate( + final ParseObjectStore from, final ParseObjectStore to) { + return from.getAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final T object = task.getResult(); + if (object == null) { + return task; + } + + return Task.whenAll(Arrays.asList( + from.deleteAsync(), + to.setAsync(object) + )).continueWith(new Continuation() { + @Override + public T then(Task task) throws Exception { + return object; + } + }); + } + }); + } + + private final String className; + private final String pinName; + private final ParseObjectStore legacy; + + public OfflineObjectStore(Class clazz, String pinName, ParseObjectStore legacy) { + this(getSubclassingController().getClassName(clazz), pinName, legacy); + } + + public OfflineObjectStore(String className, String pinName, ParseObjectStore legacy) { + this.className = className; + this.pinName = pinName; + this.legacy = legacy; + } + + @Override + public Task setAsync(final T object) { + return ParseObject.unpinAllInBackground(pinName).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return object.pinInBackground(pinName, false); + } + }); + } + + @Override + public Task getAsync() { + // We need to set `ignoreACLs` since we can't use ACLs without the current user. + ParseQuery query = ParseQuery.getQuery(className) + .fromPin(pinName) + .ignoreACLs(); + return query.findInBackground().onSuccessTask(new Continuation, Task>() { + @Override + public Task then(Task> task) throws Exception { + List results = task.getResult(); + if (results != null) { + if (results.size() == 1) { + return Task.forResult(results.get(0)); + } else { + return ParseObject.unpinAllInBackground(pinName).cast(); + } + } + return Task.forResult(null); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + T ldsObject = task.getResult(); + if (ldsObject != null) { + return task; + } + + return migrate(legacy, OfflineObjectStore.this).cast(); + } + }); + } + + @Override + public Task existsAsync() { + // We need to set `ignoreACLs` since we can't use ACLs without the current user. + ParseQuery query = ParseQuery.getQuery(className) + .fromPin(pinName) + .ignoreACLs(); + return query.countInBackground().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + boolean exists = task.getResult() == 1; + if (exists) { + return Task.forResult(true); + } + return legacy.existsAsync(); + } + }); + } + + @Override + public Task deleteAsync() { + final Task ldsTask = ParseObject.unpinAllInBackground(pinName); + return Task.whenAll(Arrays.asList( + legacy.deleteAsync(), + ldsTask + )).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We only really care about the result of unpinning. + return ldsTask; + } + }); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryController.java new file mode 100644 index 0000000..90d4a45 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryController.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class OfflineQueryController extends AbstractQueryController { + + private final OfflineStore offlineStore; + private final ParseQueryController networkController; + + public OfflineQueryController(OfflineStore store, ParseQueryController network) { + offlineStore = store; + networkController = network; + } + + @Override + public Task> findAsync( + ParseQuery.State state, + ParseUser user, + Task cancellationToken) { + if (state.isFromLocalDatastore()) { + return offlineStore.findFromPinAsync(state.pinName(), state, user); + } else { + return networkController.findAsync(state, user, cancellationToken); + } + } + + @Override + public Task countAsync( + ParseQuery.State state, + ParseUser user, + Task cancellationToken) { + if (state.isFromLocalDatastore()) { + return offlineStore.countFromPinAsync(state.pinName(), state, user); + } else { + return networkController.countAsync(state, user, cancellationToken); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryLogic.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryLogic.java new file mode 100644 index 0000000..3e0c52e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryLogic.java @@ -0,0 +1,1119 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.ParseQuery.KeyConstraints; +import com.parse.ParseQuery.QueryConstraints; +import com.parse.ParseQuery.RelationConstraint; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class OfflineQueryLogic { + /** + * A query is converted into a complex hierarchy of ConstraintMatchers that evaluate whether a + * ParseObject matches each part of the query. This is done because some parts of the query (such + * as $inQuery) are much more efficient if we can do some preprocessing. This makes some parts of + * the query matching stateful. + */ + /* package */ abstract class ConstraintMatcher { + + /* package */ final ParseUser user; + + public ConstraintMatcher(ParseUser user) { + this.user = user; + } + + /* package */ abstract Task matchesAsync(T object, ParseSQLiteDatabase db); + } + + private final OfflineStore store; + + /* package */ OfflineQueryLogic(OfflineStore store) { + this.store = store; + } + + /** + * Returns an Object's value for a given key, handling any special keys like objectId. Also + * handles dot-notation for traversing into objects. + */ + private static Object getValue(Object container, String key) throws ParseException { + return getValue(container, key, 0); + } + + private static Object getValue(Object container, String key, int depth) throws ParseException { + if (key.contains(".")) { + String[] parts = key.split("\\.", 2); + Object value = getValue(container, parts[0], depth + 1); + /* + * Only Maps and JSONObjects can be dotted into for getting values, so we should reject + * anything like ParseObjects and arrays. + */ + if (!(value == null || value == JSONObject.NULL || value instanceof Map || value instanceof JSONObject)) { + // Technically, they can search inside the REST representation of some nested objects. + if (depth > 0) { + Object restFormat = null; + try { + restFormat = PointerEncoder.get().encode(value); + } catch (Exception e) { + // Well, if we couldn't encode it, it's not searchable. + } + if (restFormat instanceof JSONObject) { + return getValue(restFormat, parts[1], depth + 1); + } + } + throw new ParseException(ParseException.INVALID_QUERY, String.format("Key %s is invalid.", + key)); + } + return getValue(value, parts[1], depth + 1); + } + + if (container instanceof ParseObject) { + final ParseObject object = (ParseObject) container; + + // The object needs to have been fetched already if we are going to sort by one of its fields. + if (!object.isDataAvailable()) { + throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s", + key)); + } + + // Handle special keys for ParseObjects. + switch (key) { + case "objectId": + return object.getObjectId(); + case "createdAt": + case "_created_at": + return object.getCreatedAt(); + case "updatedAt": + case "_updated_at": + return object.getUpdatedAt(); + default: + return object.get(key); + } + + } else if (container instanceof JSONObject) { + return ((JSONObject) container).opt(key); + + } else if (container instanceof Map) { + return ((Map) container).get(key); + + } else if (container == JSONObject.NULL) { + return null; + + } else if (container == null) { + return null; + + } else { + throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s", key)); + } + } + + /** + * General purpose compareTo that figures out the right types to use. The arguments should be + * atomic values to compare, such as Dates, Strings, or Numbers -- not composite objects or + * arrays. + */ + private static int compareTo(Object lhs, Object rhs) { + boolean lhsIsNullOrUndefined = (lhs == JSONObject.NULL || lhs == null); + boolean rhsIsNullOrUndefined = (rhs == JSONObject.NULL || rhs == null); + + if (lhsIsNullOrUndefined || rhsIsNullOrUndefined) { + if (!lhsIsNullOrUndefined) { + return 1; + } else if (!rhsIsNullOrUndefined) { + return -1; + } else { + return 0; + } + } else if (lhs instanceof Date && rhs instanceof Date) { + return ((Date) lhs).compareTo((Date) rhs); + } else if (lhs instanceof String && rhs instanceof String) { + return ((String) lhs).compareTo((String) rhs); + } else if (lhs instanceof Number && rhs instanceof Number) { + return Numbers.compare((Number) lhs, (Number) rhs); + } else { + throw new IllegalArgumentException( + String.format("Cannot compare %s against %s", lhs, rhs)); + } + } + + /** + * A decider decides whether the given value matches the given constraint. + */ + private interface Decider { + boolean decide(Object constraint, Object value); + } + + /** + * Returns true if decider returns true for any value in the given list. + */ + private static boolean compareList(Object constraint, List values, Decider decider) { + for (Object value : values) { + if (decider.decide(constraint, value)) { + return true; + } + } + return false; + } + + /** + * Returns true if decider returns true for any value in the given list. + */ + private static boolean compareArray(Object constraint, JSONArray values, Decider decider) { + for (int i = 0; i < values.length(); ++i) { + try { + if (decider.decide(constraint, values.get(i))) { + return true; + } + } catch (JSONException e) { + // This can literally never happen. + throw new RuntimeException(e); + } + } + return false; + } + + /** + * + * Returns true if the decider returns true for the given value and the given constraint. This + * method handles Mongo's logic where an item can match either an item itself, or any item within + * the item, if the item is an array. + */ + private static boolean compare(Object constraint, Object value, Decider decider) { + if (value instanceof List) { + return compareList(constraint, (List) value, decider); + } else if (value instanceof JSONArray) { + return compareArray(constraint, (JSONArray) value, decider); + } else { + return decider.decide(constraint, value); + } + } + + /** + * Implements simple equality constraints. This emulates Mongo's behavior where "equals" can mean + * array containment. + */ + private static boolean matchesEqualConstraint(Object constraint, Object value) { + if (constraint == null || value == null) { + return constraint == value; + } + + if (constraint instanceof Number && value instanceof Number) { + return compareTo(constraint, value) == 0; + } + + if (constraint instanceof ParseGeoPoint && value instanceof ParseGeoPoint) { + ParseGeoPoint lhs = (ParseGeoPoint) constraint; + ParseGeoPoint rhs = (ParseGeoPoint) value; + return lhs.getLatitude() == rhs.getLatitude() + && lhs.getLongitude() == rhs.getLongitude(); + } + + if (constraint instanceof ParsePolygon && value instanceof ParsePolygon) { + ParsePolygon lhs = (ParsePolygon) constraint; + ParsePolygon rhs = (ParsePolygon) value; + return lhs.equals(rhs); + } + + return compare(constraint, value, new Decider() { + @Override + public boolean decide(Object constraint, Object value) { + return constraint.equals(value); + } + }); + } + + /** + * Matches $ne constraints. + */ + private static boolean matchesNotEqualConstraint(Object constraint, Object value) { + return !matchesEqualConstraint(constraint, value); + } + + /** + * Matches $lt constraints. + */ + private static boolean matchesLessThanConstraint(Object constraint, Object value) { + return compare(constraint, value, new Decider() { + @Override + public boolean decide(Object constraint, Object value) { + if (value == null || value == JSONObject.NULL) { + return false; + } + return compareTo(constraint, value) > 0; + } + }); + } + + /** + * Matches $lte constraints. + */ + private static boolean matchesLessThanOrEqualToConstraint(Object constraint, Object value) { + return compare(constraint, value, new Decider() { + @Override + public boolean decide(Object constraint, Object value) { + if (value == null || value == JSONObject.NULL) { + return false; + } + return compareTo(constraint, value) >= 0; + } + }); + } + + /** + * Matches $gt constraints. + */ + private static boolean matchesGreaterThanConstraint(Object constraint, Object value) { + return compare(constraint, value, new Decider() { + @Override + public boolean decide(Object constraint, Object value) { + if (value == null || value == JSONObject.NULL) { + return false; + } + return compareTo(constraint, value) < 0; + } + }); + } + + /** + * Matches $gte constraints. + */ + private static boolean matchesGreaterThanOrEqualToConstraint(Object constraint, Object value) { + return compare(constraint, value, new Decider() { + @Override + public boolean decide(Object constraint, Object value) { + if (value == null || value == JSONObject.NULL) { + return false; + } + return compareTo(constraint, value) <= 0; + } + }); + } + + /** + * Matches $in constraints. + * $in returns true if the intersection of value and constraint is not an empty set. + */ + private static boolean matchesInConstraint(Object constraint, Object value) { + if (constraint instanceof Collection) { + for (Object requiredItem : (Collection)constraint) { + if (matchesEqualConstraint(requiredItem, value)) { + return true; + } + } + return false; + } + throw new IllegalArgumentException("Constraint type not supported for $in queries."); + } + + /** + * Matches $nin constraints. + */ + private static boolean matchesNotInConstraint(Object constraint, Object value) { + return !matchesInConstraint(constraint, value); + } + + /** + * Matches $all constraints. + */ + private static boolean matchesAllConstraint(Object constraint, Object value) { + if (value == null || value == JSONObject.NULL) { + return false; + } + + if (!(value instanceof Collection)) { + throw new IllegalArgumentException("Value type not supported for $all queries."); + } + + if (constraint instanceof Collection) { + for (Object requiredItem : (Collection) constraint) { + if (!matchesEqualConstraint(requiredItem, value)) { + return false; + } + } + return true; + } + throw new IllegalArgumentException("Constraint type not supported for $all queries."); + } + + /** + * Matches $regex constraints. + */ + private static boolean matchesRegexConstraint(Object constraint, Object value, String options) + throws ParseException { + if (value == null || value == JSONObject.NULL) { + return false; + } + + if (options == null) { + options = ""; + } + + if (!options.matches("^[imxs]*$")) { + throw new ParseException(ParseException.INVALID_QUERY, String.format( + "Invalid regex options: %s", options)); + } + + int flags = 0; + if (options.contains("i")) { + flags = flags | Pattern.CASE_INSENSITIVE; + } + if (options.contains("m")) { + flags = flags | Pattern.MULTILINE; + } + if (options.contains("x")) { + flags = flags | Pattern.COMMENTS; + } + if (options.contains("s")) { + flags = flags | Pattern.DOTALL; + } + + String regex = (String) constraint; + Pattern pattern = Pattern.compile(regex, flags); + Matcher matcher = pattern.matcher((String) value); + return matcher.find(); + } + + /** + * Matches $exists constraints. + */ + private static boolean matchesExistsConstraint(Object constraint, Object value) { + /* + * In the Android SDK, null means "undefined", and JSONObject.NULL means "null". + */ + if (constraint != null && (Boolean) constraint) { + return value != null && value != JSONObject.NULL; + } else { + return value == null || value == JSONObject.NULL; + } + } + + /** + * Matches $nearSphere constraints. + */ + private static boolean matchesNearSphereConstraint(Object constraint, Object value, + Double maxDistance) { + if (value == null || value == JSONObject.NULL) { + return false; + } + if (maxDistance == null) { + return true; + } + ParseGeoPoint point1 = (ParseGeoPoint) constraint; + ParseGeoPoint point2 = (ParseGeoPoint) value; + return point1.distanceInRadiansTo(point2) <= maxDistance; + } + + /** + * Matches $within constraints. + */ + private static boolean matchesWithinConstraint(Object constraint, Object value) + throws ParseException { + if (value == null || value == JSONObject.NULL) { + return false; + } + + @SuppressWarnings("unchecked") + HashMap> constraintMap = + (HashMap>) constraint; + ArrayList box = constraintMap.get("$box"); + ParseGeoPoint southwest = box.get(0); + ParseGeoPoint northeast = box.get(1); + ParseGeoPoint target = (ParseGeoPoint) value; + + if (northeast.getLongitude() < southwest.getLongitude()) { + throw new ParseException(ParseException.INVALID_QUERY, + "whereWithinGeoBox queries cannot cross the International Date Line."); + } + if (northeast.getLatitude() < southwest.getLatitude()) { + throw new ParseException(ParseException.INVALID_QUERY, + "The southwest corner of a geo box must be south of the northeast corner."); + } + if (northeast.getLongitude() - southwest.getLongitude() > 180) { + throw new ParseException(ParseException.INVALID_QUERY, + "Geo box queries larger than 180 degrees in longitude are not supported. " + + "Please check point order."); + } + + return (target.getLatitude() >= southwest.getLatitude() + && target.getLatitude() <= northeast.getLatitude() + && target.getLongitude() >= southwest.getLongitude() + && target.getLongitude() <= northeast.getLongitude()); + } + + /** + * Matches $geoIntersects constraints. + */ + private static boolean matchesGeoIntersectsConstraint(Object constraint, Object value) + throws ParseException { + if (value == null || value == JSONObject.NULL) { + return false; + } + + @SuppressWarnings("unchecked") + HashMap constraintMap = + (HashMap) constraint; + ParseGeoPoint point = constraintMap.get("$point"); + ParsePolygon target = (ParsePolygon) value; + return target.containsPoint(point); + } + + /** + * Matches $geoWithin constraints. + */ + private static boolean matchesGeoWithinConstraint(Object constraint, Object value) + throws ParseException { + if (value == null || value == JSONObject.NULL) { + return false; + } + + @SuppressWarnings("unchecked") + HashMap> constraintMap = + (HashMap>) constraint; + List points = constraintMap.get("$polygon"); + ParsePolygon polygon = new ParsePolygon(points); + ParseGeoPoint point = (ParseGeoPoint) value; + return polygon.containsPoint(point); + } + /** + * Returns true iff the given value matches the given operator and constraint. + * + * @throws UnsupportedOperationException + * if the operator is not one this function can handle. + */ + private static boolean matchesStatelessConstraint(String operator, Object constraint, + Object value, KeyConstraints allKeyConstraints) throws ParseException { + switch (operator) { + case "$ne": + return matchesNotEqualConstraint(constraint, value); + + case "$lt": + return matchesLessThanConstraint(constraint, value); + + case "$lte": + return matchesLessThanOrEqualToConstraint(constraint, value); + + case "$gt": + return matchesGreaterThanConstraint(constraint, value); + + case "$gte": + return matchesGreaterThanOrEqualToConstraint(constraint, value); + + case "$in": + return matchesInConstraint(constraint, value); + + case "$nin": + return matchesNotInConstraint(constraint, value); + + case "$all": + return matchesAllConstraint(constraint, value); + + case "$regex": + String regexOptions = (String) allKeyConstraints.get("$options"); + return matchesRegexConstraint(constraint, value, regexOptions); + + case "$options": + // No need to do anything. This is handled by $regex. + return true; + + case "$exists": + return matchesExistsConstraint(constraint, value); + + case "$nearSphere": + Double maxDistance = (Double) allKeyConstraints.get("$maxDistance"); + return matchesNearSphereConstraint(constraint, value, maxDistance); + + case "$maxDistance": + // No need to do anything. This is handled by $nearSphere. + return true; + + case "$within": + return matchesWithinConstraint(constraint, value); + + case "$geoWithin": + return matchesGeoWithinConstraint(constraint, value); + + case "$geoIntersects": + return matchesGeoIntersectsConstraint(constraint, value); + + default: + throw new UnsupportedOperationException(String.format( + "The offline store does not yet support the %s operator.", operator)); + } + } + + private abstract class SubQueryMatcher extends ConstraintMatcher { + private final ParseQuery.State subQuery; + private Task> subQueryResults = null; + + public SubQueryMatcher(ParseUser user, ParseQuery.State subQuery) { + super(user); + this.subQuery = subQuery; + } + + @Override + public Task matchesAsync(final T object, ParseSQLiteDatabase db) { + /* + * As an optimization, we do this lazily. Then we may not have to do it at all, if this part + * of the query gets short-circuited. + */ + if (subQueryResults == null) { + //TODO (grantland): We need to pass through the original pin we were limiting the parent + // query on. + subQueryResults = store.findAsync(subQuery, user, null, db); + } + return subQueryResults.onSuccess(new Continuation, Boolean>() { + @Override + public Boolean then(Task> task) throws ParseException { + return matches(object, task.getResult()); + } + }); + } + + protected abstract boolean matches(T object, List results) throws ParseException; + } + + /** + * Creates a matcher that handles $inQuery constraints. + */ + private ConstraintMatcher createInQueryMatcher(ParseUser user, + Object constraint, final String key) { + // TODO(grantland): Convert builder to state t6941155 + @SuppressWarnings("unchecked") + ParseQuery.State query = ((ParseQuery.State.Builder) constraint).build(); + return new SubQueryMatcher(user, query) { + @Override + protected boolean matches(T object, List results) throws ParseException { + Object value = getValue(object, key); + return matchesInConstraint(results, value); + } + }; + } + + /** + * Creates a matcher that handles $notInQuery constraints. + */ + private ConstraintMatcher createNotInQueryMatcher(ParseUser user, + Object constraint, final String key) { + final ConstraintMatcher inQueryMatcher = createInQueryMatcher(user, constraint, key); + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + return inQueryMatcher.matchesAsync(object, db).onSuccess(new Continuation() { + @Override + public Boolean then(Task task) throws Exception { + return !task.getResult(); + } + }); + } + }; + } + + /** + * Creates a matcher that handles $select constraints. + */ + private ConstraintMatcher createSelectMatcher(ParseUser user, + Object constraint, final String key) { + Map constraintMap = (Map) constraint; + // TODO(grantland): Convert builder to state t6941155 + @SuppressWarnings("unchecked") + ParseQuery.State query = ((ParseQuery.State.Builder) constraintMap.get("query")).build(); + final String resultKey = (String) constraintMap.get("key"); + return new SubQueryMatcher(user, query) { + @Override + protected boolean matches(T object, List results) throws ParseException { + Object value = getValue(object, key); + for (T result : results) { + Object resultValue = getValue(result, resultKey); + if (matchesEqualConstraint(value, resultValue)) { + return true; + } + } + return false; + } + }; + } + + /** + * Creates a matcher that handles $dontSelect constraints. + */ + private ConstraintMatcher createDontSelectMatcher(ParseUser user, + Object constraint, final String key) { + final ConstraintMatcher selectMatcher = createSelectMatcher(user, constraint, key); + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + return selectMatcher.matchesAsync(object, db).onSuccess(new Continuation() { + @Override + public Boolean then(Task task) throws Exception { + return !task.getResult(); + } + }); + } + }; + } + + /* + * Creates a matcher for a particular constraint operator. + */ + private ConstraintMatcher createMatcher(ParseUser user, + final String operator, final Object constraint, final String key, + final KeyConstraints allKeyConstraints) { + switch (operator) { + case "$inQuery": + return createInQueryMatcher(user, constraint, key); + + case "$notInQuery": + return createNotInQueryMatcher(user, constraint, key); + + case "$select": + return createSelectMatcher(user, constraint, key); + + case "$dontSelect": + return createDontSelectMatcher(user, constraint, key); + + default: + /* + * All of the other operators we know about are stateless, so return a simple matcher. + */ + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + try { + Object value = getValue(object, key); + return Task.forResult(matchesStatelessConstraint(operator, constraint, value, + allKeyConstraints)); + } catch (ParseException e) { + return Task.forError(e); + } + } + }; + } + } + + /** + * Handles $or queries. + */ + private ConstraintMatcher createOrMatcher(ParseUser user, + ArrayList queries) { + // Make a list of all the matchers to OR together. + final ArrayList> matchers = new ArrayList<>(); + for (QueryConstraints constraints : queries) { + ConstraintMatcher matcher = createMatcher(user, constraints); + matchers.add(matcher); + } + /* + * Now OR together the constraints for each query. + */ + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(final T object, final ParseSQLiteDatabase db) { + Task task = Task.forResult(false); + for (final ConstraintMatcher matcher : matchers) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.getResult()) { + return task; + } + return matcher.matchesAsync(object, db); + } + }); + } + return task; + } + }; + + } + + /** + * Returns a ConstraintMatcher that return true iff the object matches QueryConstraints. This + * takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we + * want to reuse them whenever possible. + */ + private ConstraintMatcher createMatcher(ParseUser user, + QueryConstraints queryConstraints) { + // Make a list of all the matchers to AND together. + final ArrayList> matchers = new ArrayList<>(); + for (final String key : queryConstraints.keySet()) { + final Object queryConstraintValue = queryConstraints.get(key); + + if (key.equals("$or")) { + /* + * A set of queries to be OR-ed together. + */ + @SuppressWarnings("unchecked") + ConstraintMatcher matcher = + createOrMatcher(user, (ArrayList) queryConstraintValue); + matchers.add(matcher); + + } else if (queryConstraintValue instanceof KeyConstraints) { + /* + * It's a set of constraints that should be AND-ed together. + */ + KeyConstraints keyConstraints = (KeyConstraints) queryConstraintValue; + for (String operator : keyConstraints.keySet()) { + final Object keyConstraintValue = keyConstraints.get(operator); + ConstraintMatcher matcher = + createMatcher(user, operator, keyConstraintValue, key, keyConstraints); + matchers.add(matcher); + } + + } else if (queryConstraintValue instanceof RelationConstraint) { + /* + * It's a $relatedTo constraint. + */ + final RelationConstraint relation = (RelationConstraint) queryConstraintValue; + matchers.add(new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + return Task.forResult(relation.getRelation().hasKnownObject(object)); + } + }); + + } else { + /* + * It's not a set of constraints, so it's just a value to compare against. + */ + matchers.add(new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + Object objectValue; + try { + objectValue = getValue(object, key); + } catch (ParseException e) { + return Task.forError(e); + } + return Task.forResult(matchesEqualConstraint(queryConstraintValue, objectValue)); + } + }); + } + } + + /* + * Now AND together the constraints for each key. + */ + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(final T object, final ParseSQLiteDatabase db) { + Task task = Task.forResult(true); + for (final ConstraintMatcher matcher : matchers) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!task.getResult()) { + return task; + } + return matcher.matchesAsync(object, db); + } + }); + } + return task; + } + }; + } + + /** + * Returns true iff the object is visible based on its read ACL and the given user objectId. + */ + /* package */ static boolean hasReadAccess(ParseUser user, T object) { + if (user == object) { + return true; + } + + ParseACL acl = object.getACL(); + if (acl == null) { + return true; + } + if (acl.getPublicReadAccess()) { + return true; + } + if (user != null && acl.getReadAccess(user)) { + return true; + } + // TODO: Implement roles. + return false; + } + + /** + * Returns true iff the object is visible based on its read ACL and the given user objectId. + */ + /* package */ static boolean hasWriteAccess(ParseUser user, T object) { + if (user == object) { + return true; + } + + ParseACL acl = object.getACL(); + if (acl == null) { + return true; + } + if (acl.getPublicWriteAccess()) { + return true; + } + if (user != null && acl.getWriteAccess(user)) { + return true; + } + // TODO: Implement roles. + return false; + } + + /** + * Returns a ConstraintMatcher that return true iff the object matches the given query's + * constraints. This takes in a SQLiteDatabase connection because SQLite is finicky about nesting + * connections, so we want to reuse them whenever possible. + * + * @param state The query. + * @param user The user we are testing ACL access for. + * @param Subclass of ParseObject. + * @return A new instance of ConstraintMatcher. + */ + /* package */ ConstraintMatcher createMatcher( + ParseQuery.State state, final ParseUser user) { + final boolean ignoreACLs = state.ignoreACLs(); + final ConstraintMatcher constraintMatcher = createMatcher(user, state.constraints()); + + return new ConstraintMatcher(user) { + @Override + public Task matchesAsync(T object, ParseSQLiteDatabase db) { + if (!ignoreACLs && !hasReadAccess(user, object)) { + return Task.forResult(false); + } + return constraintMatcher.matchesAsync(object, db); + } + }; + } + + /** + * Sorts the given array based on the parameters of the given query. + */ + /* package */ static void sort(List results, ParseQuery.State state) + throws ParseException { + final List keys = state.order(); + // Do some error checking just for maximum compatibility with the server. + for (String key : state.order()) { + if (!key.matches("^-?[A-Za-z][A-Za-z0-9_]*$")) { + if (!"_created_at".equals(key) && !"_updated_at".equals(key)) { + throw new ParseException(ParseException.INVALID_KEY_NAME, String.format( + "Invalid key name: \"%s\".", key)); + } + } + } + + // See if there's a $nearSphere constraint that will override the other sort parameters. + String mutableNearSphereKey = null; + ParseGeoPoint mutableNearSphereValue = null; + for (String queryKey : state.constraints().keySet()) { + Object queryKeyConstraints = state.constraints().get(queryKey); + if (queryKeyConstraints instanceof KeyConstraints) { + KeyConstraints keyConstraints = (KeyConstraints) queryKeyConstraints; + if (keyConstraints.containsKey("$nearSphere")) { + mutableNearSphereKey = queryKey; + mutableNearSphereValue = (ParseGeoPoint) keyConstraints.get("$nearSphere"); + } + } + } + final String nearSphereKey = mutableNearSphereKey; + final ParseGeoPoint nearSphereValue = mutableNearSphereValue; + + // If there's nothing to sort based on, then don't do anything. + if (keys.size() == 0 && mutableNearSphereKey == null) { + return; + } + + /* + * TODO(klimt): Test whether we allow dotting into objects for sorting. + */ + + Collections.sort(results, new Comparator() { + @Override + public int compare(T lhs, T rhs) { + if (nearSphereKey != null) { + ParseGeoPoint lhsPoint; + ParseGeoPoint rhsPoint; + try { + lhsPoint = (ParseGeoPoint) getValue(lhs, nearSphereKey); + rhsPoint = (ParseGeoPoint) getValue(rhs, nearSphereKey); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + // GeoPoints can't be null if there's a $nearSphere. + double lhsDistance = lhsPoint.distanceInRadiansTo(nearSphereValue); + double rhsDistance = rhsPoint.distanceInRadiansTo(nearSphereValue); + if (lhsDistance != rhsDistance) { + return (lhsDistance - rhsDistance > 0) ? 1 : -1; + } + } + + for (String key : keys) { + boolean descending = false; + if (key.startsWith("-")) { + descending = true; + key = key.substring(1); + } + + Object lhsValue; + Object rhsValue; + try { + lhsValue = getValue(lhs, key); + rhsValue = getValue(rhs, key); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + int result; + try { + result = compareTo(lhsValue, rhsValue); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(String.format("Unable to sort by key %s.", key), e); + } + if (result != 0) { + return descending ? -result : result; + } + } + return 0; + } + }); + } + + /** + * Makes sure that the object specified by path, relative to container, is fetched. + */ + private static Task fetchIncludeAsync( + final OfflineStore store, + final Object container, + final String path, + final ParseSQLiteDatabase db) + throws ParseException { + // If there's no object to include, that's fine. + if (container == null) { + return Task.forResult(null); + } + + // If the container is a list or array, fetch all the sub-items. + if (container instanceof Collection) { + Collection collection = (Collection) container; + // We do the fetches in series because it makes it easier to fail on the first error. + Task task = Task.forResult(null); + for (final Object item : collection) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return fetchIncludeAsync(store, item, path, db); + } + }); + } + return task; + } else if (container instanceof JSONArray) { + final JSONArray array = (JSONArray) container; + // We do the fetches in series because it makes it easier to fail on the first error. + Task task = Task.forResult(null); + for (int i = 0; i < array.length(); ++i) { + final int index = i; + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return fetchIncludeAsync(store, array.get(index), path, db); + } + }); + } + return task; + } + + // If we've reached the end of the path, then actually do the fetch. + if (path == null) { + if (JSONObject.NULL.equals(container)) { + // Accept JSONObject.NULL value in included field. We swallow it silently instead of + // throwing an exception. + return Task.forResult(null); + } else if (container instanceof ParseObject) { + ParseObject object = (ParseObject) container; + return store.fetchLocallyAsync(object, db).makeVoid(); + } else { + return Task.forError(new ParseException( + ParseException.INVALID_NESTED_KEY, "include is invalid for non-ParseObjects")); + } + } + + // Descend into the container and try again. + + String[] parts = path.split("\\.", 2); + final String key = parts[0]; + final String rest = (parts.length > 1 ? parts[1] : null); + + // Make sure the container is fetched. + return Task. forResult(null).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (container instanceof ParseObject) { + // Make sure this object is fetched before descending into it. + return fetchIncludeAsync(store, container, null, db).onSuccess(new Continuation() { + @Override + public Object then(Task task) throws Exception { + return ((ParseObject) container).get(key); + } + }); + } else if (container instanceof Map) { + return Task.forResult(((Map) container).get(key)); + } else if (container instanceof JSONObject) { + return Task.forResult(((JSONObject) container).opt(key)); + } else if (JSONObject.NULL.equals(container)) { + // Accept JSONObject.NULL value in included field. We swallow it silently instead of + // throwing an exception. + return null; + } else { + return Task.forError(new IllegalStateException("include is invalid")); + } + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return fetchIncludeAsync(store, task.getResult(), rest, db); + } + }); + } + + /** + * Makes sure all of the objects included by the given query get fetched. + */ + /* package */ static Task fetchIncludesAsync( + final OfflineStore store, + final T object, + ParseQuery.State state, + final ParseSQLiteDatabase db) { + Set includes = state.includes(); + // We do the fetches in series because it makes it easier to fail on the first error. + Task task = Task.forResult(null); + for (final String include : includes) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return fetchIncludeAsync(store, object, include, db); + } + }); + } + return task; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineSQLiteOpenHelper.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineSQLiteOpenHelper.java new file mode 100644 index 0000000..4faa1f2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineSQLiteOpenHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +/** + * This class just wraps a SQLiteDatabase with a better API. SQLite has a few limitations that this + * class works around. The primary problem is that if you call getWritableDatabase from multiple + * places, they all return the same instance, so you can't call "close" until you are done with all + * of them. SQLite also doesn't allow multiple transactions at the same time. We don't need + * transactions yet, but when we do, they will be part of this class. For convenience, this class + * also wraps database methods with methods that run them on a background thread and return a task. + */ +/** package */ class OfflineSQLiteOpenHelper extends ParseSQLiteOpenHelper { + + /** + * The table that stores all ParseObjects. + */ + /* package */ static final String TABLE_OBJECTS = "ParseObjects"; + + /** + * Various keys in the table of ParseObjects. + */ + /* package */ /* package */ static final String KEY_UUID = "uuid"; + /* package */ static final String KEY_CLASS_NAME = "className"; + /* package */ static final String KEY_OBJECT_ID = "objectId"; + /* package */ static final String KEY_JSON = "json"; + /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "isDeletingEventually"; + + /** + * The table that stores all Dependencies. + */ + /* package */ static final String TABLE_DEPENDENCIES = "Dependencies"; + + /** + * Various keys in the table of Dependencies. + */ + //TODO (grantland): rename this since we use UUIDs as keys now. root_uuid? + /* package */ static final String KEY_KEY = "key"; + // static final String KEY_UUID = "uuid"; + + /** + * The SQLite Database name. + */ + private static final String DATABASE_NAME = "ParseOfflineStore"; + private static final int DATABASE_VERSION = 4; + + /** + * Creates a new helper for the database. + */ + public OfflineSQLiteOpenHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + /** + * Initializes the schema for the database. + */ + private void createSchema(SQLiteDatabase db) { + String sql; + + sql = "CREATE TABLE " + TABLE_OBJECTS + " (" + + KEY_UUID + " TEXT PRIMARY KEY, " + + KEY_CLASS_NAME + " TEXT NOT NULL, " + + KEY_OBJECT_ID + " TEXT, " + + KEY_JSON + " TEXT, " + + KEY_IS_DELETING_EVENTUALLY + " INTEGER DEFAULT 0, " + + "UNIQUE(" + KEY_CLASS_NAME + ", " + KEY_OBJECT_ID + ")" + + ");"; + db.execSQL(sql); + + sql = "CREATE TABLE " + TABLE_DEPENDENCIES + " (" + + KEY_KEY + " TEXT NOT NULL, " + + KEY_UUID + " TEXT NOT NULL, " + + "PRIMARY KEY(" + KEY_KEY + ", " + KEY_UUID + ")" + + ");"; + db.execSQL(sql); + } + + /** + * Called when the database is first created. + */ + @Override + public void onCreate(SQLiteDatabase db) { + createSchema(db); + } + + /** + * Called when the version number in code doesn't match the one on disk. + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // do nothing + } + + /** + * Drops all tables and then recreates the schema. + */ + public void clearDatabase(Context context) { + context.deleteDatabase(DATABASE_NAME); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineStore.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineStore.java new file mode 100644 index 0000000..620f2eb --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineStore.java @@ -0,0 +1,1567 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; +import android.util.Pair; + +import com.parse.OfflineQueryLogic.ConstraintMatcher; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; + +import bolts.Capture; +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** package */ class OfflineStore { + + /** + * SQLite has a max of 999 SQL variables in a single statement. + */ + private static final int MAX_SQL_VARIABLES = 999; + + /** + * Extends the normal JSON -> ParseObject decoding to also deal with placeholders for new objects + * that have been saved offline. + */ + private class OfflineDecoder extends ParseDecoder { + // A map of UUID -> Task that will be finished once the given ParseObject is loaded. + // The Tasks should all be finished before decode is called. + private Map> offlineObjects; + + private OfflineDecoder(Map> offlineObjects) { + this.offlineObjects = offlineObjects; + } + + @Override + public Object decode(Object object) { + // If we see an offline id, make sure to decode it. + if (object instanceof JSONObject + && ((JSONObject) object).optString("__type").equals("OfflineObject")) { + String uuid = ((JSONObject) object).optString("uuid"); + return offlineObjects.get(uuid).getResult(); + } + + /* + * Embedded objects can't show up here, because we never stored them that way offline. + */ + + return super.decode(object); + } + } + + /** + * An encoder that can encode objects that are available offline. After using this encoder, you + * must call whenFinished() and wait for its result to be finished before the results of the + * encoding will be valid. + */ + private class OfflineEncoder extends ParseEncoder { + private ParseSQLiteDatabase db; + private ArrayList> tasks = new ArrayList<>(); + private final Object tasksLock = new Object(); + + /** + * Creates an encoder. + * + * @param db + * A database connection to use. + */ + public OfflineEncoder(ParseSQLiteDatabase db) { + this.db = db; + } + + /** + * The results of encoding an object with this encoder will not be valid until the task returned + * by this method is finished. + */ + public Task whenFinished() { + return Task.whenAll(tasks).continueWithTask(new Continuation>() { + @Override + public Task then(Task ignore) throws Exception { + synchronized (tasksLock) { + // It might be better to return an aggregate error here. + for (Task task : tasks) { + if (task.isFaulted() || task.isCancelled()) { + return task; + } + } + tasks.clear(); + return Task.forResult(null); + } + } + }); + } + + /** + * Implements an encoding strategy for Parse Objects that uses offline ids when necessary. + */ + @Override + public JSONObject encodeRelatedObject(ParseObject object) { + try { + if (object.getObjectId() != null) { + JSONObject result = new JSONObject(); + result.put("__type", "Pointer"); + result.put("objectId", object.getObjectId()); + result.put("className", object.getClassName()); + return result; + } + + final JSONObject result = new JSONObject(); + result.put("__type", "OfflineObject"); + synchronized (tasksLock) { + tasks.add(getOrCreateUUIDAsync(object, db).onSuccess(new Continuation() { + @Override + public Void then(Task task) throws Exception { + result.put("uuid", task.getResult()); + return null; + } + })); + } + return result; + } catch (JSONException e) { + // This can literally never happen. + throw new RuntimeException(e); + } + } + } + + // Lock for all members of the store. + final private Object lock = new Object(); + + // Helper for accessing the database. + final private OfflineSQLiteOpenHelper helper; + + /** + * In-memory map of UUID -> ParseObject. This is used so that we can always return the same + * instance for a given object. The only objects in this map are ones that are in the database. + */ + final private WeakValueHashMap uuidToObjectMap = new WeakValueHashMap<>(); + + /** + * In-memory map of ParseObject -> UUID. This is used to that when we see an unsaved ParseObject + * that's already in the database, we can update the same record in the database. It stores a Task + * instead of the String, because one thread may want to reserve the spot. Once the task is + * finished, there will be a row for this UUID in the database. + */ + final private WeakHashMap> objectToUuidMap = new WeakHashMap<>(); + + /** + * In-memory set of ParseObjects that have been fetched from the local database already. If the + * object is in the map, a fetch of it has been started. If the value is a finished task, then the + * fetch was completed. + */ + final private WeakHashMap> fetchedObjects = new WeakHashMap<>(); + + /** + * Used by the static method to create the singleton. + */ + /* package */ OfflineStore(Context context) { + this(new OfflineSQLiteOpenHelper(context)); + } + + /* package */ OfflineStore(OfflineSQLiteOpenHelper helper) { + this.helper = helper; + } + + /** + * Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object + * and adds a new row to the database for the object with no data. + */ + private Task getOrCreateUUIDAsync(final ParseObject object, ParseSQLiteDatabase db) { + final String newUUID = UUID.randomUUID().toString(); + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + + synchronized (lock) { + Task uuidTask = objectToUuidMap.get(object); + if (uuidTask != null) { + return uuidTask; + } + + // The object doesn't have a UUID yet, so we're gonna have to make one. + objectToUuidMap.put(object, tcs.getTask()); + uuidToObjectMap.put(newUUID, object); + fetchedObjects.put(object, tcs.getTask().onSuccess(new Continuation() { + @Override + public ParseObject then(Task task) throws Exception { + return object; + } + })); + } + + /* + * We need to put a placeholder row in the database so that later on, the save can just be an + * update. This could be a pointer to an object that itself never gets saved offline, in which + * case the consumer will just have to deal with that. + */ + ContentValues values = new ContentValues(); + values.put(OfflineSQLiteOpenHelper.KEY_UUID, newUUID); + values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, object.getClassName()); + db.insertOrThrowAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values).continueWith( + new Continuation() { + @Override + public Void then(Task task) throws Exception { + // This will signal that the UUID does represent a row in the database. + tcs.setResult(newUUID); + return null; + } + }); + + return tcs.getTask(); + } + + /** + * Gets an unfetched pointer to an object in the db, based on its uuid. The object may or may not + * be in memory, but it must be in the database. If it is already in memory, that instance will be + * returned. Since this is only for creating pointers to objects that are referenced by other + * objects in the data store, that's a fair assumption. + * + * @param uuid + * The object to retrieve. + * @param db + * The database instance to retrieve from. + * @return The object with that UUID. + */ + private Task getPointerAsync(final String uuid, + ParseSQLiteDatabase db) { + synchronized (lock) { + @SuppressWarnings("unchecked") + T existing = (T) uuidToObjectMap.get(uuid); + if (existing != null) { + return Task.forResult(existing); + } + } + + /* + * We want to just return the pointer, but we have to look in the database to know if there's + * something with this classname and object id already. + */ + + String[] select = { OfflineSQLiteOpenHelper.KEY_CLASS_NAME, OfflineSQLiteOpenHelper.KEY_OBJECT_ID }; + String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; + String[] args = { uuid }; + return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess( + new Continuation() { + @Override + public T then(Task task) throws Exception { + Cursor cursor = task.getResult(); + cursor.moveToFirst(); + if (cursor.isAfterLast()) { + cursor.close(); + throw new IllegalStateException("Attempted to find non-existent uuid " + uuid); + } + + synchronized (lock) { + // We need to check again since another task might have come around and added it to + // the map. + //TODO (grantland): Maybe we should insert a Task that is resolved when the query + // completes like we do in getOrCreateUUIDAsync? + @SuppressWarnings("unchecked") + T existing = (T) uuidToObjectMap.get(uuid); + if (existing != null) { + return existing; + } + + String className = cursor.getString(0); + String objectId = cursor.getString(1); + cursor.close(); + @SuppressWarnings("unchecked") + T pointer = (T) ParseObject.createWithoutData(className, objectId); + /* + * If it doesn't have an objectId, we don't really need the UUID, and this simplifies + * some other logic elsewhere if we only update the map for new objects. + */ + if (objectId == null) { + uuidToObjectMap.put(uuid, pointer); + objectToUuidMap.put(pointer, Task.forResult(uuid)); + } + return pointer; + } + } + }); + } + + /** + * Runs a ParseQuery against the store's contents. + * + * @return The objects that match the query's constraints. + */ + /* package for OfflineQueryLogic */ Task> findAsync( + ParseQuery.State query, + ParseUser user, + ParsePin pin, + ParseSQLiteDatabase db) { + return findAsync(query, user, pin, false, db); + } + + /** + * Runs a ParseQuery against the store's contents. May cause any instances of T to get fetched + * from the offline database. TODO(klimt): Should the query consider objects that are in memory, + * but not in the offline store? + * + * @param query The query. + * @param user The user making the query. + * @param pin (Optional) The pin we are querying across. If null, all pins. + * @param isCount True if we are doing a count. + * @param db The SQLiteDatabase. + * @param Subclass of ParseObject. + * @return The objects that match the query's constraints. + */ + private Task> findAsync( + final ParseQuery.State query, + final ParseUser user, + final ParsePin pin, + final boolean isCount, + final ParseSQLiteDatabase db) { + /* + * This is currently unused, but is here to allow future querying across objects that are in the + * process of being deleted eventually. + */ + final boolean includeIsDeletingEventually = false; + + final OfflineQueryLogic queryLogic = new OfflineQueryLogic(this); + + final List results = new ArrayList<>(); + + Task queryTask; + if (pin == null) { + String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS; + String[] select = { OfflineSQLiteOpenHelper.KEY_UUID }; + String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?"; + if (!includeIsDeletingEventually) { + where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0"; + } + String[] args = { query.className() }; + + queryTask = db.queryAsync(table, select, where, args); + } else { + Task uuidTask = objectToUuidMap.get(pin); + if (uuidTask == null) { + // Pin was never saved locally, therefore there won't be any results. + return Task.forResult(results); + } + + queryTask = uuidTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String uuid = task.getResult(); + + String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + " A " + + " INNER JOIN " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + " B " + + " ON A." + OfflineSQLiteOpenHelper.KEY_UUID + "=B." + OfflineSQLiteOpenHelper.KEY_UUID; + String[] select = {"A." + OfflineSQLiteOpenHelper.KEY_UUID}; + String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" + + " AND " + OfflineSQLiteOpenHelper.KEY_KEY + "=?"; + if (!includeIsDeletingEventually) { + where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0"; + } + String[] args = { query.className(), uuid }; + + return db.queryAsync(table, select, where, args); + } + }); + } + + return queryTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + Cursor cursor = task.getResult(); + List uuids = new ArrayList<>(); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + uuids.add(cursor.getString(0)); + } + cursor.close(); + + // Find objects that match the where clause. + final ConstraintMatcher matcher = queryLogic.createMatcher(query, user); + + Task checkedAllObjects = Task.forResult(null); + for (final String uuid : uuids) { + final Capture object = new Capture<>(); + + checkedAllObjects = checkedAllObjects.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getPointerAsync(uuid, db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + object.set(task.getResult()); + return fetchLocallyAsync(object.get(), db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!object.get().isDataAvailable()) { + return Task.forResult(false); + } + return matcher.matchesAsync(object.get(), db); + } + }).onSuccess(new Continuation() { + @Override + public Void then(Task task) { + if (task.getResult()) { + results.add(object.get()); + } + return null; + } + }); + } + + return checkedAllObjects; + } + }).onSuccessTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + // Sort by any sort operators. + OfflineQueryLogic.sort(results, query); + + // Apply the skip. + List trimmedResults = results; + int skip = query.skip(); + if (!isCount && skip >= 0) { + skip = Math.min(query.skip(), trimmedResults.size()); + trimmedResults = trimmedResults.subList(skip, trimmedResults.size()); + } + + // Trim to the limit. + int limit = query.limit(); + if (!isCount && limit >= 0 && trimmedResults.size() > limit) { + trimmedResults = trimmedResults.subList(0, limit); + } + + // Fetch the includes. + Task fetchedIncludesTask = Task.forResult(null); + for (final T object : trimmedResults) { + fetchedIncludesTask = fetchedIncludesTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return OfflineQueryLogic.fetchIncludesAsync(OfflineStore.this, object, query, db); + } + }); + } + + final List finalTrimmedResults = trimmedResults; + return fetchedIncludesTask.onSuccess(new Continuation>() { + @Override + public List then(Task task) throws Exception { + return finalTrimmedResults; + } + }); + } + }); + } + + /** + * Gets the data for the given object from the offline database. Returns a task that will be + * completed if data for the object was available. If the object is not in the cache, the task + * will be faulted, with a CACHE_MISS error. + * + * @param object + * The object to fetch. + * @param db + * A database connection to use. + */ + /* package for OfflineQueryLogic */ Task fetchLocallyAsync( + final T object, + final ParseSQLiteDatabase db) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + Task uuidTask; + + synchronized (lock) { + if (fetchedObjects.containsKey(object)) { + /* + * The object has already been fetched from the offline store, so any data that's in there + * is already reflected in the in-memory version. There's nothing more to do. + */ + //noinspection unchecked + return (Task) fetchedObjects.get(object); + } + + /* + * Put a placeholder so that anyone else who attempts to fetch this object will just wait for + * this call to finish doing it. + */ + //noinspection unchecked + fetchedObjects.put(object, (Task) tcs.getTask()); + + uuidTask = objectToUuidMap.get(object); + } + String className = object.getClassName(); + String objectId = object.getObjectId(); + + /* + * If this gets set, then it will contain data from the offline store that needs to be merged + * into the existing object in memory. + */ + Task jsonStringTask = Task.forResult(null); + + if (objectId == null) { + // This Object has never been saved to Parse. + if (uuidTask == null) { + /* + * This object was not pulled from the data store or previously saved to it, so there's + * nothing that can be fetched from it. This isn't an error, because it's really convenient + * to try to fetch objects from the offline store just to make sure they are up-to-date, and + * we shouldn't force developers to specially handle this case. + */ + } else { + /* + * This object is a new ParseObject that is known to the data store, but hasn't been + * fetched. The only way this could happen is if the object had previously been stored in + * the offline store, then the object was removed from memory (maybe by rebooting), and then + * a object with a pointer to it was fetched, so we only created the pointer. We need to + * pull the data out of the database using the UUID. + */ + final String[] select = { OfflineSQLiteOpenHelper.KEY_JSON }; + final String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; + final Capture uuid = new Capture<>(); + jsonStringTask = uuidTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + uuid.set(task.getResult()); + String[] args = { uuid.get() }; + return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args); + } + }).onSuccess(new Continuation() { + @Override + public String then(Task task) throws Exception { + Cursor cursor = task.getResult(); + cursor.moveToFirst(); + if (cursor.isAfterLast()) { + cursor.close(); + throw new IllegalStateException("Attempted to find non-existent uuid " + uuid.get()); + } + String json = cursor.getString(0); + cursor.close(); + + return json; + } + }); + } + } else { + if (uuidTask != null) { + /* + * This object is an existing ParseObject, and we must've already pulled its data out of the + * offline store, or else we wouldn't know its UUID. This should never happen. + */ + tcs.setError(new IllegalStateException("This object must have already been " + + "fetched from the local datastore, but isn't marked as fetched.")); + synchronized (lock) { + // Forget we even tried to fetch this object, so that retries will actually... retry. + fetchedObjects.remove(object); + } + return tcs.getTask(); + } + + /* + * We've got a pointer to an existing ParseObject, but we've never pulled its data out of the + * offline store. Since fetching from the server forces a fetch from the offline store, that + * means this is a pointer. We need to try to find any existing entry for this object in the + * database. + */ + String[] select = { OfflineSQLiteOpenHelper.KEY_JSON, OfflineSQLiteOpenHelper.KEY_UUID }; + String where = + String.format("%s = ? AND %s = ?", OfflineSQLiteOpenHelper.KEY_CLASS_NAME, + OfflineSQLiteOpenHelper.KEY_OBJECT_ID); + String[] args = { className, objectId }; + jsonStringTask = + db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess( + new Continuation() { + @Override + public String then(Task task) throws Exception { + Cursor cursor = task.getResult(); + cursor.moveToFirst(); + if (cursor.isAfterLast()) { + /* + * This is a pointer that came from Parse that references an object that has + * never been saved in the offline store before. This just means there's no data + * in the store that needs to be merged into the object. + */ + cursor.close(); + throw new ParseException(ParseException.CACHE_MISS, + "This object is not available in the offline cache."); + } + + // we should fetch its data and record its UUID for future reference. + String jsonString = cursor.getString(0); + String newUUID = cursor.getString(1); + cursor.close(); + + synchronized (lock) { + /* + * It's okay to put this object into the uuid map. No one will try to fetch + * it, because it's already in the fetchedObjects map. And no one will try to + * save to it without fetching it first, so everything should be just fine. + */ + objectToUuidMap.put(object, Task.forResult(newUUID)); + uuidToObjectMap.put(newUUID, object); + } + + return jsonString; + } + }); + } + + return jsonStringTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String jsonString = task.getResult(); + if (jsonString == null) { + /* + * This means we tried to fetch an object from the database that was never actually saved + * locally. This probably means that its parent object was saved locally and we just + * created a pointer to this object. This should be considered a cache miss. + */ + return Task.forError(new ParseException(ParseException.CACHE_MISS, + "Attempted to fetch an object offline which was never saved to the offline cache.")); + } + final JSONObject json; + try { + /* + * We can assume that whatever is in the database is the last known server state. The only + * things to maintain from the in-memory object are any changes since the object was last + * put in the database. + */ + json = new JSONObject(jsonString); + } catch (JSONException e) { + return Task.forError(e); + } + + // Fetch all the offline objects before we decode. + final Map> offlineObjects = new HashMap<>(); + + (new ParseTraverser() { + @Override + protected boolean visit(Object object) { + if (object instanceof JSONObject + && ((JSONObject) object).optString("__type").equals("OfflineObject")) { + String uuid = ((JSONObject) object).optString("uuid"); + offlineObjects.put(uuid, getPointerAsync(uuid, db)); + } + return true; + } + }).setTraverseParseObjects(false).setYieldRoot(false).traverse(json); + + return Task.whenAll(offlineObjects.values()).onSuccess(new Continuation() { + @Override + public Void then(Task task) throws Exception { + object.mergeREST(object.getState(), json, new OfflineDecoder(offlineObjects)); + return null; + } + }); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isCancelled()) { + tcs.setCancelled(); + } else if (task.isFaulted()) { + tcs.setError(task.getError()); + } else { + tcs.setResult(object); + } + return tcs.getTask(); + } + }); + } + + /** + * Gets the data for the given object from the offline database. Returns a task that will be + * completed if data for the object was available. If the object is not in the cache, the task + * will be faulted, with a CACHE_MISS error. + * + * @param object + * The object to fetch. + */ + /* package */ Task fetchLocallyAsync(final T object) { + return runWithManagedConnection(new SQLiteDatabaseCallable>() { + @Override + public Task call(ParseSQLiteDatabase db) { + return fetchLocallyAsync(object, db); + } + }); + } + + /** + * Stores a single object in the local database. If the object is a pointer, isn't dirty, and has + * an objectId already, it may not be saved, since it would provide no useful data. + * + * @param object + * The object to save. + * @param db + * A database connection to use. + */ + private Task saveLocallyAsync( + final String key, final ParseObject object, final ParseSQLiteDatabase db) { + // If this is just a clean, unfetched pointer known to Parse, then there is nothing to save. + if (object.getObjectId() != null && !object.isDataAvailable() && !object.hasChanges() + && !object.hasOutstandingOperations()) { + return Task.forResult(null); + } + + final Capture uuidCapture = new Capture<>(); + + // Make sure we have a UUID for the object to be saved. + return getOrCreateUUIDAsync(object, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String uuid = task.getResult(); + uuidCapture.set(uuid); + return updateDataForObjectAsync(uuid, object, db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ContentValues values = new ContentValues(); + values.put(OfflineSQLiteOpenHelper.KEY_KEY, key); + values.put(OfflineSQLiteOpenHelper.KEY_UUID, uuidCapture.get()); + return db.insertWithOnConflict(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, values, + SQLiteDatabase.CONFLICT_IGNORE); + } + }); + } + + /** + * Stores an object (and optionally, every object it points to recursively) in the local database. + * If any of the objects have not been fetched from Parse, they will not be stored. However, if + * they have changed data, the data will be retained. To get the objects back later, you can use a + * ParseQuery with a cache policy that uses the local cache, or you can create an unfetched + * pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. If you modify + * the object after saving it locally, such as by fetching it or saving it, those changes will + * automatically be applied to the cache. + * + * Any objects previously stored with the same key will be removed from the local database. + * + * @param object Root object + * @param includeAllChildren {@code true} to recursively save all pointers. + * @param db DB connection + * @return A Task that will be resolved when saving is complete + */ + private Task saveLocallyAsync( + final ParseObject object, final boolean includeAllChildren, final ParseSQLiteDatabase db) { + final ArrayList objectsInTree = new ArrayList<>(); + // Fetch all objects locally in case they are being re-added + if (!includeAllChildren) { + objectsInTree.add(object); + } else { + (new ParseTraverser() { + @Override + protected boolean visit(Object object) { + if (object instanceof ParseObject) { + objectsInTree.add((ParseObject) object); + } + return true; + } + }).setYieldRoot(true).setTraverseParseObjects(true).traverse(object); + } + + return saveLocallyAsync(object, objectsInTree, db); + } + + + private Task saveLocallyAsync( + final ParseObject object, List children, final ParseSQLiteDatabase db) { + final List objects = children != null + ? new ArrayList<>(children) + : new ArrayList(); + if (!objects.contains(object)) { + objects.add(object); + } + + // Call saveLocallyAsync for each of them individually. + final List> tasks = new ArrayList<>(); + for (ParseObject obj : objects) { + tasks.add(fetchLocallyAsync(obj, db).makeVoid()); + } + + return Task.whenAll(tasks).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return objectToUuidMap.get(object); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String uuid = task.getResult(); + if (uuid == null) { + // The root object was never stored in the offline store, so nothing to unpin. + return null; + } + + // Delete all objects locally corresponding to the key we're trying to use in case it was + // used before (overwrite) + return unpinAsync(uuid, db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getOrCreateUUIDAsync(object, db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String uuid = task.getResult(); + + // Call saveLocallyAsync for each of them individually. + final List> tasks = new ArrayList<>(); + for (ParseObject obj : objects) { + tasks.add(saveLocallyAsync(uuid, obj, db)); + } + + return Task.whenAll(tasks); + } + }); + } + + private Task unpinAsync(final ParseObject object, final ParseSQLiteDatabase db) { + Task uuidTask = objectToUuidMap.get(object); + if (uuidTask == null) { + // The root object was never stored in the offline store, so nothing to unpin. + return Task.forResult(null); + } + return uuidTask.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String uuid = task.getResult(); + if (uuid == null) { + // The root object was never stored in the offline store, so nothing to unpin. + return Task.forResult(null); + } + return unpinAsync(uuid, db); + } + }); + } + + private Task unpinAsync(final String key, final ParseSQLiteDatabase db) { + final List uuidsToDelete = new LinkedList<>(); + // A continueWithTask that ends with "return task" is essentially a try-finally. + return Task.forResult((Void) null).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1 + String sql = "SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + + " WHERE " + OfflineSQLiteOpenHelper.KEY_KEY + "=? AND " + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + + " SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + + " GROUP BY " + OfflineSQLiteOpenHelper.KEY_UUID + + " HAVING COUNT(" + OfflineSQLiteOpenHelper.KEY_UUID + ")=1" + + ")"; + String[] args = {key}; + return db.rawQueryAsync(sql, args); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // DELETE FROM Objects + + Cursor cursor = task.getResult(); + while (cursor.moveToNext()) { + uuidsToDelete.add(cursor.getString(0)); + } + cursor.close(); + + return deleteObjects(uuidsToDelete, db); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // DELETE FROM Dependencies + String where = OfflineSQLiteOpenHelper.KEY_KEY + "=?"; + String[] args = {key}; + return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args); + } + }).onSuccess(new Continuation() { + @Override + public Void then(Task task) throws Exception { + synchronized (lock) { + // Remove uuids from memory + for (String uuid : uuidsToDelete) { + ParseObject object = uuidToObjectMap.get(uuid); + if (object != null) { + objectToUuidMap.remove(object); + uuidToObjectMap.remove(uuid); + } + } + } + return null; + } + }); + } + + private Task deleteObjects(final List uuids, final ParseSQLiteDatabase db) { + if (uuids.size() <= 0) { + return Task.forResult(null); + } + + // SQLite has a max 999 SQL variables in a statement, so we need to split it up into manageable + // chunks. We can do this because we're already in a transaction. + if (uuids.size() > MAX_SQL_VARIABLES) { + return deleteObjects(uuids.subList(0, MAX_SQL_VARIABLES), db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return deleteObjects(uuids.subList(MAX_SQL_VARIABLES, uuids.size()), db); + } + }); + } + + String[] placeholders = new String[uuids.size()]; + for (int i = 0; i < placeholders.length; i++) { + placeholders[i] = "?"; + } + String where = OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + TextUtils.join(",", placeholders) + ")"; + // dynamic args + String[] args = uuids.toArray(new String[uuids.size()]); + return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args); + } + + /** + * Takes an object that has been fetched from the database before and updates it with whatever + * data is in memory. This will only be used when data comes back from the server after a fetch or + * a save. + */ + /* package */ Task updateDataForObjectAsync(final ParseObject object) { + Task fetched; + // Make sure the object is fetched. + synchronized (lock) { + fetched = fetchedObjects.get(object); + if (fetched == null) { + return Task.forError(new IllegalStateException( + "An object cannot be updated if it wasn't fetched.")); + } + } + return fetched.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted()) { + // Catch CACHE_MISS + //noinspection ThrowableResultOfMethodCallIgnored + if (task.getError() instanceof ParseException + && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) { + return Task.forResult(null); + } + return task.makeVoid(); + } + + return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseSQLiteDatabase db = task.getResult(); + return db.beginTransactionAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return updateDataForObjectAsync(object, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return db.setTransactionSuccessfulAsync(); + } + }).continueWithTask(new Continuation>() { + // } finally { + @Override + public Task then(Task task) throws Exception { + db.endTransactionAsync(); + db.closeAsync(); + return task; + } + }); + } + }); + } + }); + } + }); + } + + private Task updateDataForObjectAsync( + final ParseObject object, + final ParseSQLiteDatabase db) { + // Make sure the object has a UUID. + Task uuidTask; + synchronized (lock) { + uuidTask = objectToUuidMap.get(object); + if (uuidTask == null) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return Task.forResult(null); + } + } + return uuidTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String uuid = task.getResult(); + return updateDataForObjectAsync(uuid, object, db); + } + }); + } + + private Task updateDataForObjectAsync( + final String uuid, + final ParseObject object, + final ParseSQLiteDatabase db) { + // Now actually encode the object as JSON. + OfflineEncoder encoder = new OfflineEncoder(db); + final JSONObject json = object.toRest(encoder); + + return encoder.whenFinished().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Put the JSON in the database. + String className = object.getClassName(); + String objectId = object.getObjectId(); + int isDeletingEventually = json.getInt(ParseObject.KEY_IS_DELETING_EVENTUALLY); + + final ContentValues values = new ContentValues(); + values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, className); + values.put(OfflineSQLiteOpenHelper.KEY_JSON, json.toString()); + if (objectId != null) { + values.put(OfflineSQLiteOpenHelper.KEY_OBJECT_ID, objectId); + } + values.put(OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); + String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; + String[] args = {uuid}; + return db.updateAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values, where, args).makeVoid(); + } + }); + } + + /* package */ Task deleteDataForObjectAsync(final ParseObject object) { + return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseSQLiteDatabase db = task.getResult(); + return db.beginTransactionAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return deleteDataForObjectAsync(object, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return db.setTransactionSuccessfulAsync(); + } + }).continueWithTask(new Continuation>() { + // } finally { + @Override + public Task then(Task task) throws Exception { + db.endTransactionAsync(); + db.closeAsync(); + return task; + } + }); + } + }); + } + }); + } + + private Task deleteDataForObjectAsync(final ParseObject object, final ParseSQLiteDatabase db) { + final Capture uuid = new Capture<>(); + + // Make sure the object has a UUID. + Task uuidTask; + synchronized (lock) { + uuidTask = objectToUuidMap.get(object); + if (uuidTask == null) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return Task.forResult(null); + } + } + uuidTask = uuidTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + uuid.set(task.getResult()); + return task; + } + }); + + // If the object was the root of a pin, unpin it. + Task unpinTask = uuidTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Find all the roots for this object. + String[] select = { OfflineSQLiteOpenHelper.KEY_KEY }; + String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; + String[] args = { uuid.get() }; + return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, select, where, args); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Try to unpin this object from the pin label if it's a root of the ParsePin. + Cursor cursor = task.getResult(); + List uuids = new ArrayList<>(); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + uuids.add(cursor.getString(0)); + } + cursor.close(); + + List> tasks = new ArrayList<>(); + for (final String uuid : uuids) { + Task unpinTask = getPointerAsync(uuid, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParsePin pin = (ParsePin) task.getResult(); + return fetchLocallyAsync(pin, db); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParsePin pin = task.getResult(); + + List modified = pin.getObjects(); + if (modified == null || !modified.contains(object)) { + return task.makeVoid(); + } + + modified.remove(object); + if (modified.size() == 0) { + return unpinAsync(uuid, db); + } + + pin.setObjects(modified); + return saveLocallyAsync(pin, true, db); + } + }); + tasks.add(unpinTask); + } + + return Task.whenAll(tasks); + } + }); + + // Delete the object from the Local Datastore in case it wasn't the root of a pin. + return unpinTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; + String[] args = {uuid.get()}; + return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; + String[] args = {uuid.get()}; + return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (lock) { + // Clean up + //TODO (grantland): we should probably clean up uuidToObjectMap and objectToUuidMap, but + // getting the uuid requires a task and things might get a little funky... + fetchedObjects.remove(object); + } + return task; + } + }); + } + + //region ParsePin + + private Task getParsePin(final String name, ParseSQLiteDatabase db) { + ParseQuery.State query = new ParseQuery.State.Builder<>(ParsePin.class) + .whereEqualTo(ParsePin.KEY_NAME, name) + .build(); + + /* We need to call directly to the OfflineStore since we don't want/need a user to query for + * ParsePins + */ + return findAsync(query, null, null, db).onSuccess(new Continuation, ParsePin>() { + @Override + public ParsePin then(Task> task) throws Exception { + ParsePin pin = null; + if (task.getResult() != null && task.getResult().size() > 0) { + pin = task.getResult().get(0); + } + + //TODO (grantland): What do we do if there are more than 1 result? + + if (pin == null) { + pin = ParseObject.create(ParsePin.class); + pin.setName(name); + } + return pin; + } + }); + } + + /* package */ Task pinAllObjectsAsync( + final String name, + final List objects, + final boolean includeChildren) { + return runWithManagedTransaction(new SQLiteDatabaseCallable>() { + @Override + public Task call(ParseSQLiteDatabase db) { + return pinAllObjectsAsync(name, objects, includeChildren, db); + } + }); + } + + private Task pinAllObjectsAsync( + final String name, + final List objects, + final boolean includeChildren, + final ParseSQLiteDatabase db) { + if (objects == null || objects.size() == 0) { + return Task.forResult(null); + } + + return getParsePin(name, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParsePin pin = task.getResult(); + + //TODO (grantland): change to use relations. currently the related PO are only getting saved + // offline as pointers. +// ParseRelation relation = pin.getRelation(KEY_OBJECTS); +// relation.add(object); + + // Hack to store collections in a pin + List modified = pin.getObjects(); + if (modified == null) { + modified = new ArrayList(objects); + } else { + for (ParseObject object : objects) { + if (!modified.contains(object)) { + modified.add(object); + } + } + } + pin.setObjects(modified); + + if (includeChildren) { + return saveLocallyAsync(pin, true, db); + } + return saveLocallyAsync(pin, pin.getObjects(), db); + } + }); + } + + /* package */ Task unpinAllObjectsAsync( + final String name, + final List objects) { + return runWithManagedTransaction(new SQLiteDatabaseCallable>() { + @Override + public Task call(ParseSQLiteDatabase db) { + return unpinAllObjectsAsync(name, objects, db); + } + }); + } + + private Task unpinAllObjectsAsync( + String name, + final List objects, + final ParseSQLiteDatabase db) { + if (objects == null || objects.size() == 0) { + return Task.forResult(null); + } + + return getParsePin(name, db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParsePin pin = task.getResult(); + + //TODO (grantland): change to use relations. currently the related PO are only getting saved + // offline as pointers. +// ParseRelation relation = pin.getRelation(KEY_OBJECTS); +// relation.remove(object); + + // Hack to store collections in a pin + List modified = pin.getObjects(); + if (modified == null) { + // Unpin a pin that doesn't exist. Wat? + return Task.forResult(null); + } + + modified.removeAll(objects); + if (modified.size() == 0) { + return unpinAsync(pin, db); + } + pin.setObjects(modified); + + return saveLocallyAsync(pin, true, db); + } + }); + } + + /* package */ Task unpinAllObjectsAsync(final String name) { + return runWithManagedTransaction(new SQLiteDatabaseCallable>() { + @Override + public Task call(ParseSQLiteDatabase db) { + return unpinAllObjectsAsync(name, db); + } + }); + } + + private Task unpinAllObjectsAsync(final String name, final ParseSQLiteDatabase db) { + return getParsePin(name, db).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted()) { + return task.makeVoid(); + } + ParsePin pin = task.getResult(); + return unpinAsync(pin, db); + } + }); + } + + /* package */ Task> findFromPinAsync( + final String name, + final ParseQuery.State state, + final ParseUser user) { + return runWithManagedConnection(new SQLiteDatabaseCallable>>() { + @Override + public Task> call(ParseSQLiteDatabase db) { + return findFromPinAsync(name, state, user, db); + } + }); + } + + private Task> findFromPinAsync( + final String name, + final ParseQuery.State state, + final ParseUser user, + final ParseSQLiteDatabase db) { + Task task; + if (name != null) { + task = getParsePin(name, db); + } else { + task = Task.forResult(null); + } + return task.onSuccessTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + ParsePin pin = task.getResult(); + return findAsync(state, user, pin, false, db); + } + }); + } + + /* package */ Task countFromPinAsync( + final String name, + final ParseQuery.State state, + final ParseUser user) { + return runWithManagedConnection(new SQLiteDatabaseCallable>() { + @Override + public Task call(ParseSQLiteDatabase db) { + return countFromPinAsync(name, state, user, db); + } + }); + } + + private Task countFromPinAsync( + final String name, + final ParseQuery.State state, + final ParseUser user, + final ParseSQLiteDatabase db) { + Task task; + if (name != null) { + task = getParsePin(name, db); + } else { + task = Task.forResult(null); + } + return task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParsePin pin = task.getResult(); + return findAsync(state, user, pin, true, db).onSuccess(new Continuation, Integer>() { + @Override + public Integer then(Task> task) throws Exception { + return task.getResult().size(); + } + }); + } + }); + } + + //endregion + + //region Single Instance + + /** + * In-memory map of (className, objectId) -> ParseObject. This is used so that we can always + * return the same instance for a given object. Objects in this map may or may not be in the + * database. + */ + private final WeakValueHashMap, ParseObject> + classNameAndObjectIdToObjectMap = new WeakValueHashMap<>(); + + /** + * This should be called by the ParseObject constructor notify the store that there is an object + * with this className and objectId. + */ + /* package */ void registerNewObject(ParseObject object) { + synchronized (lock) { + String objectId = object.getObjectId(); + if (objectId != null) { + String className = object.getClassName(); + Pair classNameAndObjectId = Pair.create(className, objectId); + classNameAndObjectIdToObjectMap.put(classNameAndObjectId, object); + } + } + } + + /* package */ void unregisterObject(ParseObject object) { + synchronized (lock) { + String objectId = object.getObjectId(); + if (objectId != null) { + classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), objectId)); + } + } + } + + /** + * This should only ever be called from ParseObject.createWithoutData(). + * + * @return a pair of ParseObject and Boolean. The ParseObject is the object. The Boolean is true + * iff the object was newly created. + */ + /* package */ ParseObject getObject(String className, String objectId) { + if (objectId == null) { + throw new IllegalStateException("objectId cannot be null."); + } + + Pair classNameAndObjectId = Pair.create(className, objectId); + // This lock should never be held by anyone doing disk or database access. + synchronized (lock) { + return classNameAndObjectIdToObjectMap.get(classNameAndObjectId); + } + } + + /** + * When an object is finished saving, it gets an objectId. Then it should call this method to + * clean up the bookeeping around ids. + */ + /* package */ void updateObjectId(ParseObject object, String oldObjectId, String newObjectId) { + if (oldObjectId != null) { + if (oldObjectId.equals(newObjectId)) { + return; + } + /** + * Special case for re-saving installation if it was deleted on the server + * @see ParseInstallation#saveAsync(String, Task) + */ + if (object instanceof ParseInstallation + && newObjectId == null) { + synchronized (lock) { + classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), oldObjectId)); + } + return; + } else { + throw new RuntimeException("objectIds cannot be changed in offline mode."); + } + } + + String className = object.getClassName(); + Pair classNameAndNewObjectId = Pair.create(className, newObjectId); + + synchronized (lock) { + // See if there's already an entry for the new object id. + ParseObject existing = classNameAndObjectIdToObjectMap.get(classNameAndNewObjectId); + if (existing != null && existing != object) { + throw new RuntimeException("Attempted to change an objectId to one that's " + + "already known to the Offline Store."); + } + + // Okay, all clear to add the new reference. + classNameAndObjectIdToObjectMap.put(classNameAndNewObjectId, object); + } + } + + //endregion + + /** + * Wraps SQLite operations with a managed SQLite connection. + */ + private Task runWithManagedConnection(final SQLiteDatabaseCallable> callable) { + return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseSQLiteDatabase db = task.getResult(); + return callable.call(db).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + db.closeAsync(); + return task; + } + }); + } + }); + } + + /** + * Wraps SQLite operations with a managed SQLite connection and transaction. + */ + private Task runWithManagedTransaction(final SQLiteDatabaseCallable> callable) { + return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseSQLiteDatabase db = task.getResult(); + return db.beginTransactionAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return callable.call(db).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return db.setTransactionSuccessfulAsync(); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + db.endTransactionAsync(); + db.closeAsync(); + return task; + } + }); + } + }); + } + }); + } + + private interface SQLiteDatabaseCallable { + T call(ParseSQLiteDatabase db); + } + + /* + * Methods for testing. + */ + + /** + * Clears all in-memory caches so that data must be retrieved from disk. + */ + void simulateReboot() { + synchronized (lock) { + uuidToObjectMap.clear(); + objectToUuidMap.clear(); + classNameAndObjectIdToObjectMap.clear(); + fetchedObjects.clear(); + } + } + + /** + * Clears the database on disk. + */ + void clearDatabase(Context context) { + helper.clearDatabase(context); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PLog.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PLog.java new file mode 100644 index 0000000..86502ea --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PLog.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.util.Log; + +/** package */ class PLog { + public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE; + + private static int logLevel = Integer.MAX_VALUE; + + /** + * Sets the level of logging to display, where each level includes all those below it. The default + * level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link Log#ERROR} + * or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is + * logged. The levels are: + *
    + *
  • {@link Log#VERBOSE}
  • + *
  • {@link Log#DEBUG}
  • + *
  • {@link Log#INFO}
  • + *
  • {@link Log#WARN}
  • + *
  • {@link Log#ERROR}
  • + *
  • {@link #LOG_LEVEL_NONE}
  • + *
+ * + * @param logLevel + * The level of logcat logging that Parse should do. + */ + public static void setLogLevel(int logLevel) { + PLog.logLevel = logLevel; + } + + /** + * Returns the level of logging that will be displayed. + */ + public static int getLogLevel() { + return logLevel; + } + + private static void log(int messageLogLevel, String tag, String message, Throwable tr) { + if (messageLogLevel >= logLevel) { + if (tr == null) { + Log.println(logLevel, tag, message); + } else { + Log.println(logLevel, tag, message + '\n' + Log.getStackTraceString(tr)); + } + } + } + + /* package */ static void v(String tag, String message, Throwable tr) { + log(Log.VERBOSE, tag, message, tr); + } + + /* package */ static void v(String tag, String message) { + v(tag, message, null); + } + + /* package */ static void d(String tag, String message, Throwable tr) { + log(Log.DEBUG, tag, message, tr); + } + + /* package */ static void d(String tag, String message) { + d(tag, message, null); + } + + /* package */ static void i(String tag, String message, Throwable tr) { + log(Log.INFO, tag, message, tr); + } + + /* package */ static void i(String tag, String message) { + i(tag, message, null); + } + + /* package */ static void w(String tag, String message, Throwable tr) { + log(Log.WARN, tag, message, tr); + } + + /* package */ static void w(String tag, String message) { + w(tag, message, null); + } + + /* package */ static void e(String tag, String message, Throwable tr) { + log(Log.ERROR, tag, message, tr); + } + + /* package */ static void e(String tag, String message) { + e(tag, message, null); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Parse.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Parse.java new file mode 100644 index 0000000..e525ba2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/Parse.java @@ -0,0 +1,770 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; + +import bolts.Continuation; +import bolts.Task; +import okhttp3.OkHttpClient; + +/** + * The {@code Parse} class contains static functions that handle global configuration for the Parse + * library. + */ +public class Parse { + private static final String TAG = "com.parse.Parse"; + + private static final int DEFAULT_MAX_RETRIES = ParseRequest.DEFAULT_MAX_RETRIES; + + /** + * Represents an opaque configuration for the {@code Parse} SDK configuration. + */ + public static final class Configuration { + /** + * Allows for simple constructing of a {@code Configuration} object. + */ + public static final class Builder { + private Context context; + private String applicationId; + private String clientKey; + private String server; + private boolean localDataStoreEnabled; + private OkHttpClient.Builder clientBuilder; + private int maxRetries = DEFAULT_MAX_RETRIES; + + /** + * Initialize a bulider with a given context. + *

+ * This context will then be passed through to the rest of the Parse SDK for use during + * initialization. + *

+ *

+ * You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY} + * {@code meta-data} in your {@code AndroidManifest.xml}: + *

+       * <manifest ...>
+       *
+       * ...
+       *
+       *   <application ...>
+       *     <meta-data
+       *       android:name="com.parse.SERVER_URL"
+       *       android:value="@string/parse_server_url" />
+       *     <meta-data
+       *       android:name="com.parse.APPLICATION_ID"
+       *       android:value="@string/parse_app_id" />
+       *     <meta-data
+       *       android:name="com.parse.CLIENT_KEY"
+       *       android:value="@string/parse_client_key" />
+       *
+       *       ...
+       *
+       *   </application>
+       * </manifest>
+       * 
+ *

+ *

+ * This will cause the values for {@code server}, {@code applicationId} and {@code clientKey} to be set to + * those defined in your manifest. + * + * @param context The active {@link Context} for your application. Cannot be null. + */ + public Builder(Context context) { + this.context = context; + + // Yes, our public API states we cannot be null. But for unit tests, it's easier just to + // support null here. + if (context != null) { + Context applicationContext = context.getApplicationContext(); + Bundle metaData = ManifestInfo.getApplicationMetadata(applicationContext); + if (metaData != null) { + server(metaData.getString(PARSE_SERVER_URL)); + applicationId = metaData.getString(PARSE_APPLICATION_ID); + clientKey = metaData.getString(PARSE_CLIENT_KEY); + } + } + } + + /** + * Set the application id to be used by Parse. + *

+ * This method is only required if you intend to use a different {@code applicationId} than + * is defined by {@code com.parse.APPLICATION_ID} in your {@code AndroidManifest.xml}. + * + * @param applicationId The application id to set. + * @return The same builder, for easy chaining. + */ + public Builder applicationId(String applicationId) { + this.applicationId = applicationId; + return this; + } + + /** + * Set the client key to be used by Parse. + *

+ * This method is only required if you intend to use a different {@code clientKey} than + * is defined by {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}. + * + * @param clientKey The client key to set. + * @return The same builder, for easy chaining. + */ + public Builder clientKey(String clientKey) { + this.clientKey = clientKey; + return this; + } + + /** + * Set the server URL to be used by Parse. + * + * @param server The server URL to set. + * @return The same builder, for easy chaining. + */ + public Builder server(String server) { + + // Add an extra trailing slash so that Parse REST commands include + // the path as part of the server URL (i.e. http://api.myhost.com/parse) + if (server != null && !server.endsWith("/")) { + server = server + "/"; + } + + this.server = server; + return this; + } + + /** + * Enable pinning in your application. This must be called before your application can use + * pinning. + * + * @return The same builder, for easy chaining. + */ + public Builder enableLocalDataStore() { + localDataStoreEnabled = true; + return this; + } + + private Builder setLocalDatastoreEnabled(boolean enabled) { + localDataStoreEnabled = enabled; + return this; + } + + /** + * Set the {@link okhttp3.OkHttpClient.Builder} to use when communicating with the Parse + * REST API + *

+ * + * @param builder The client builder, which will be modified for compatibility + * @return The same builder, for easy chaining. + */ + public Builder clientBuilder(OkHttpClient.Builder builder) { + clientBuilder = builder; + return this; + } + + /** + * Set the max number of times to retry Parse operations before deeming them a failure + *

+ * + * @param maxRetries The maximum number of times to retry. <=0 to never retry commands + * @return The same builder, for easy chaining. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Construct this builder into a concrete {@code Configuration} instance. + * + * @return A constructed {@code Configuration} object. + */ + public Configuration build() { + return new Configuration(this); + } + } + + final Context context; + final String applicationId; + final String clientKey; + final String server; + final boolean localDataStoreEnabled; + final OkHttpClient.Builder clientBuilder; + final int maxRetries; + + + private Configuration(Builder builder) { + this.context = builder.context; + this.applicationId = builder.applicationId; + this.clientKey = builder.clientKey; + this.server = builder.server; + this.localDataStoreEnabled = builder.localDataStoreEnabled; + this.clientBuilder = builder.clientBuilder; + this.maxRetries = builder.maxRetries; + } + } + + private static final String PARSE_SERVER_URL = "com.parse.SERVER_URL"; + private static final String PARSE_APPLICATION_ID = "com.parse.APPLICATION_ID"; + private static final String PARSE_CLIENT_KEY = "com.parse.CLIENT_KEY"; + + private static final Object MUTEX = new Object(); + static ParseEventuallyQueue eventuallyQueue = null; + + //region LDS + + private static boolean isLocalDatastoreEnabled; + private static OfflineStore offlineStore; + + /** + * Enable pinning in your application. This must be called before your application can use + * pinning. You must invoke {@code enableLocalDatastore(Context)} before + * {@link #initialize(Context)} : + *

+ *

+   * public class MyApplication extends Application {
+   *   public void onCreate() {
+   *     Parse.enableLocalDatastore(this);
+   *     Parse.initialize(this);
+   *   }
+   * }
+   * 
+ * + * @param context The active {@link Context} for your application. + */ + public static void enableLocalDatastore(Context context) { + if (isInitialized()) { + throw new IllegalStateException("`Parse#enableLocalDatastore(Context)` must be invoked " + + "before `Parse#initialize(Context)`"); + } + isLocalDatastoreEnabled = true; + } + + static void disableLocalDatastore() { + setLocalDatastore(null); + // We need to re-register ParseCurrentInstallationController otherwise it is still offline + // controller + ParseCorePlugins.getInstance().reset(); + } + + static OfflineStore getLocalDatastore() { + return offlineStore; + } + + static void setLocalDatastore(OfflineStore offlineStore) { + Parse.isLocalDatastoreEnabled = offlineStore != null; + Parse.offlineStore = offlineStore; + } + + public static boolean isLocalDatastoreEnabled() { + return isLocalDatastoreEnabled; + } + + //endregion + + /** + * Authenticates this client as belonging to your application. + *

+ * You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY} + * {@code meta-data} in your {@code AndroidManifest.xml}: + *

+   * <manifest ...>
+   *
+   * ...
+   *
+   *   <application ...>
+   *     <meta-data
+   *       android:name="com.parse.SERVER_URL"
+   *       android:value="@string/parse_server_url" />
+   *     <meta-data
+   *       android:name="com.parse.APPLICATION_ID"
+   *       android:value="@string/parse_app_id" />
+   *     <meta-data
+   *       android:name="com.parse.CLIENT_KEY"
+   *       android:value="@string/parse_client_key" />
+   *
+   *       ...
+   *
+   *   </application>
+   * </manifest>
+   * 
+ *

+ * This must be called before your application can use the Parse library. + * The recommended way is to put a call to {@code Parse.initialize} + * in your {@code Application}'s {@code onCreate} method: + *

+ *

+   * public class MyApplication extends Application {
+   *   public void onCreate() {
+   *     Parse.initialize(this);
+   *   }
+   * }
+   * 
+ * + * @param context The active {@link Context} for your application. + */ + public static void initialize(Context context) { + Configuration.Builder builder = new Configuration.Builder(context); + if (builder.server == null) { + throw new RuntimeException("ServerUrl not defined. " + + "You must provide ServerUrl in AndroidManifest.xml.\n" + + "\" />"); + } + if (builder.applicationId == null) { + throw new RuntimeException("ApplicationId not defined. " + + "You must provide ApplicationId in AndroidManifest.xml.\n" + + "\" />"); + } + initialize(builder + .setLocalDatastoreEnabled(isLocalDatastoreEnabled) + .build() + ); + } + + /** + * Authenticates this client as belonging to your application. + *

+ * This method is only required if you intend to use a different {@code applicationId} or + * {@code clientKey} than is defined by {@code com.parse.APPLICATION_ID} or + * {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}. + *

+ * This must be called before your + * application can use the Parse library. The recommended way is to put a call to + * {@code Parse.initialize} in your {@code Application}'s {@code onCreate} method: + *

+ *

+   * public class MyApplication extends Application {
+   *   public void onCreate() {
+   *     Parse.initialize(this, "your application id", "your client key");
+   *   }
+   * }
+   * 
+ * + * @param context The active {@link Context} for your application. + * @param applicationId The application id provided in the Parse dashboard. + * @param clientKey The client key provided in the Parse dashboard. + */ + public static void initialize(Context context, String applicationId, String clientKey) { + initialize(new Configuration.Builder(context) + .applicationId(applicationId) + .clientKey(clientKey) + .setLocalDatastoreEnabled(isLocalDatastoreEnabled) + .build() + ); + } + + public static void initialize(Configuration configuration) { + if (isInitialized()) { + PLog.w(TAG, "Parse is already initialized"); + return; + } + // NOTE (richardross): We will need this here, as ParsePlugins uses the return value of + // isLocalDataStoreEnabled() to perform additional behavior. + isLocalDatastoreEnabled = configuration.localDataStoreEnabled; + + ParsePlugins.initialize(configuration.context, configuration); + + try { + ParseRESTCommand.server = new URL(configuration.server); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + + ParseObject.registerParseSubclasses(); + + if (configuration.localDataStoreEnabled) { + offlineStore = new OfflineStore(configuration.context); + } else { + ParseKeyValueCache.initialize(configuration.context); + } + + // Make sure the data on disk for Parse is for the current + // application. + checkCacheApplicationId(); + final Context context = configuration.context; + Task.callInBackground(new Callable() { + @Override + public Void call() throws Exception { + getEventuallyQueue(context); + return null; + } + }); + + ParseFieldOperations.registerDefaultDecoders(); + + if (!allParsePushIntentReceiversInternal()) { + throw new SecurityException("To prevent external tampering to your app's notifications, " + + "all receivers registered to handle the following actions must have " + + "their exported attributes set to false: com.parse.push.intent.RECEIVE, " + + "com.parse.push.intent.OPEN, com.parse.push.intent.DELETE"); + } + + // May need to update GCM registration ID if app version has changed. + // This also primes current installation. + PushServiceUtils.initialize().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Prime current user in the background + return ParseUser.getCurrentUserAsync().makeVoid(); + } + }).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + // Prime config in the background + ParseConfig.getCurrentConfig(); + return null; + } + }, Task.BACKGROUND_EXECUTOR); + + dispatchOnParseInitialized(); + + // FYI we probably don't want to do this if we ever add other callbacks. + synchronized (MUTEX_CALLBACKS) { + Parse.callbacks = null; + } + } + + static void destroy() { + ParseEventuallyQueue queue; + synchronized (MUTEX) { + queue = eventuallyQueue; + eventuallyQueue = null; + } + if (queue != null) { + queue.onDestroy(); + } + + ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + } + + /** + * @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. + */ + static boolean isInitialized() { + return ParsePlugins.get() != null; + } + + static Context getApplicationContext() { + checkContext(); + return ParsePlugins.get().applicationContext(); + } + + /** + * Checks that each of the receivers associated with the three actions defined in + * ParsePushBroadcastReceiver (ACTION_PUSH_RECEIVE, ACTION_PUSH_OPEN, ACTION_PUSH_DELETE) has + * their exported attributes set to false. If this is the case for each of the receivers + * registered in the AndroidManifest.xml or if no receivers are registered (because we will be registering + * the default implementation of ParsePushBroadcastReceiver in PushService) then true is returned. + * Note: the reason for iterating through lists, is because you can define different receivers + * in the manifest that respond to the same intents and both all of the receivers will be triggered. + * So we want to make sure all them have the exported attribute set to false. + */ + private static boolean allParsePushIntentReceiversInternal() { + List intentReceivers = ManifestInfo.getIntentReceivers( + ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE, + ParsePushBroadcastReceiver.ACTION_PUSH_DELETE, + ParsePushBroadcastReceiver.ACTION_PUSH_OPEN); + + for (ResolveInfo resolveInfo : intentReceivers) { + if (resolveInfo.activityInfo.exported) { + return false; + } + } + return true; + } + + /** + * @deprecated Please use {@link #getParseCacheDir(String)} or {@link #getParseFilesDir(String)} + * instead. + */ + @Deprecated + static File getParseDir() { + return ParsePlugins.get().getParseDir(); + } + + static File getParseCacheDir() { + return ParsePlugins.get().getCacheDir(); + } + + static File getParseCacheDir(String subDir) { + synchronized (MUTEX) { + File dir = new File(getParseCacheDir(), subDir); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + } + + static File getParseFilesDir() { + return ParsePlugins.get().getFilesDir(); + } + + static File getParseFilesDir(String subDir) { + synchronized (MUTEX) { + File dir = new File(getParseFilesDir(), subDir); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + } + + /** + * Verifies that the data stored on disk for Parse was generated using the same application that + * is running now. + */ + static void checkCacheApplicationId() { + synchronized (MUTEX) { + String applicationId = ParsePlugins.get().applicationId(); + if (applicationId != null) { + File dir = Parse.getParseCacheDir(); + + // Make sure the current version of the cache is for this application id. + File applicationIdFile = new File(dir, "applicationId"); + if (applicationIdFile.exists()) { + // Read the file + boolean matches = false; + try { + RandomAccessFile f = new RandomAccessFile(applicationIdFile, "r"); + byte[] bytes = new byte[(int) f.length()]; + f.readFully(bytes); + f.close(); + String diskApplicationId = new String(bytes, "UTF-8"); + matches = diskApplicationId.equals(applicationId); + } catch (IOException e) { + // Hmm, the applicationId file was malformed or something. Assume it + // doesn't match. + } + + // The application id has changed, so everything on disk is invalid. + if (!matches) { + try { + ParseFileUtils.deleteDirectory(dir); + } catch (IOException e) { + // We're unable to delete the directy... + } + } + } + + // Create the version file if needed. + applicationIdFile = new File(dir, "applicationId"); + try { + FileOutputStream out = new FileOutputStream(applicationIdFile); + out.write(applicationId.getBytes("UTF-8")); + out.close(); + } catch (IOException e) { + // Nothing we can really do about it. + } + } + } + } + + /** + * Gets the shared command cache object for all ParseObjects. This command cache is used to + * locally store save commands created by the ParseObject.saveEventually(). When a new + * ParseCommandCache is instantiated, it will begin running its run loop, which will start by + * processing any commands already stored in the on-disk queue. + */ + static ParseEventuallyQueue getEventuallyQueue() { + Context context = ParsePlugins.get().applicationContext(); + return getEventuallyQueue(context); + } + + private static ParseEventuallyQueue getEventuallyQueue(Context context) { + synchronized (MUTEX) { + boolean isLocalDatastoreEnabled = Parse.isLocalDatastoreEnabled(); + if (eventuallyQueue == null + || (isLocalDatastoreEnabled && eventuallyQueue instanceof ParseCommandCache) + || (!isLocalDatastoreEnabled && eventuallyQueue instanceof ParsePinningEventuallyQueue)) { + checkContext(); + ParseHttpClient httpClient = ParsePlugins.get().restClient(); + eventuallyQueue = isLocalDatastoreEnabled + ? new ParsePinningEventuallyQueue(context, httpClient) + : new ParseCommandCache(context, httpClient); + + // We still need to clear out the old command cache even if we're using Pinning in case + // anything is left over when the user upgraded. Checking number of pending and then + // initializing should be enough. + if (isLocalDatastoreEnabled && ParseCommandCache.getPendingCount() > 0) { + new ParseCommandCache(context, httpClient); + } + } + return eventuallyQueue; + } + } + + static void checkInit() { + if (ParsePlugins.get() == null) { + throw new RuntimeException("You must call Parse.initialize(Context)" + + " before using the Parse library."); + } + + if (ParsePlugins.get().applicationId() == null) { + throw new RuntimeException("applicationId is null. " + + "You must call Parse.initialize(Context)" + + " before using the Parse library."); + } + } + + static void checkContext() { + if (ParsePlugins.get().applicationContext() == null) { + throw new RuntimeException("applicationContext is null. " + + "You must call Parse.initialize(Context)" + + " before using the Parse library."); + } + } + + static boolean hasPermission(String permission) { + return (getApplicationContext().checkCallingOrSelfPermission(permission) == + PackageManager.PERMISSION_GRANTED); + } + + static void requirePermission(String permission) { + if (!hasPermission(permission)) { + throw new IllegalStateException( + "To use this functionality, add this to your AndroidManifest.xml:\n" + + ""); + } + } + + //region ParseCallbacks + private static final Object MUTEX_CALLBACKS = new Object(); + private static Set callbacks = new HashSet<>(); + + /** + * Registers a listener to be called at the completion of {@link #initialize}. + *

+ * Throws {@link java.lang.IllegalStateException} if called after {@link #initialize}. + * + * @param listener the listener to register + */ + static void registerParseCallbacks(ParseCallbacks listener) { + if (isInitialized()) { + throw new IllegalStateException( + "You must register callbacks before Parse.initialize(Context)"); + } + + synchronized (MUTEX_CALLBACKS) { + if (callbacks == null) { + return; + } + callbacks.add(listener); + } + } + + /** + * Unregisters a listener previously registered with {@link #registerParseCallbacks}. + * + * @param listener the listener to register + */ + static void unregisterParseCallbacks(ParseCallbacks listener) { + synchronized (MUTEX_CALLBACKS) { + if (callbacks == null) { + return; + } + callbacks.remove(listener); + } + } + + private static void dispatchOnParseInitialized() { + ParseCallbacks[] callbacks = collectParseCallbacks(); + if (callbacks != null) { + for (ParseCallbacks callback : callbacks) { + callback.onParseInitialized(); + } + } + } + + private static ParseCallbacks[] collectParseCallbacks() { + ParseCallbacks[] callbacks; + synchronized (MUTEX_CALLBACKS) { + if (Parse.callbacks == null) { + return null; + } + callbacks = new ParseCallbacks[Parse.callbacks.size()]; + if (Parse.callbacks.size() > 0) { + callbacks = Parse.callbacks.toArray(callbacks); + } + } + return callbacks; + } + + interface ParseCallbacks { + void onParseInitialized(); + } + + //endregion + + //region Logging + + public static final int LOG_LEVEL_VERBOSE = Log.VERBOSE; + public static final int LOG_LEVEL_DEBUG = Log.DEBUG; + public static final int LOG_LEVEL_INFO = Log.INFO; + public static final int LOG_LEVEL_WARNING = Log.WARN; + public static final int LOG_LEVEL_ERROR = Log.ERROR; + public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE; + + /** + * Sets the level of logging to display, where each level includes all those below it. The default + * level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link #LOG_LEVEL_ERROR} + * or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is + * logged. The levels are: + *

    + *
  • {@link #LOG_LEVEL_VERBOSE}
  • + *
  • {@link #LOG_LEVEL_DEBUG}
  • + *
  • {@link #LOG_LEVEL_INFO}
  • + *
  • {@link #LOG_LEVEL_WARNING}
  • + *
  • {@link #LOG_LEVEL_ERROR}
  • + *
  • {@link #LOG_LEVEL_NONE}
  • + *
+ * + * @param logLevel The level of logcat logging that Parse should do. + */ + public static void setLogLevel(int logLevel) { + PLog.setLogLevel(logLevel); + } + + /** + * Returns the level of logging that will be displayed. + */ + public static int getLogLevel() { + return PLog.getLogLevel(); + } + + //endregion + + // Suppress constructor to prevent subclassing + private Parse() { + throw new AssertionError(); + } + + static String externalVersionName() { + return "a" + ParseObject.VERSION_NAME; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseACL.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseACL.java new file mode 100644 index 0000000..e95cfed --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseACL.java @@ -0,0 +1,611 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A {@code ParseACL} is used to control which users can access or modify a particular object. Each + * {@link ParseObject} can have its own {@code ParseACL}. You can grant read and write permissions + * separately to specific users, to groups of users that belong to roles, or you can grant + * permissions to "the public" so that, for example, any user could read a particular object but + * only a particular set of users could write to that object. + */ +public class ParseACL implements Parcelable { + private static final String PUBLIC_KEY = "*"; + private final static String UNRESOLVED_KEY = "*unresolved"; + private static final String KEY_ROLE_PREFIX = "role:"; + private static final String UNRESOLVED_USER_JSON_KEY = "unresolvedUser"; + + private static class Permissions { + private static final String READ_PERMISSION = "read"; + private static final String WRITE_PERMISSION = "write"; + + private final boolean readPermission; + private final boolean writePermission; + + /* package */ Permissions(boolean readPermission, boolean write) { + this.readPermission = readPermission; + this.writePermission = write; + } + + /* package */ Permissions(Permissions permissions) { + this.readPermission = permissions.readPermission; + this.writePermission = permissions.writePermission; + } + + /* package */ JSONObject toJSONObject() { + JSONObject json = new JSONObject(); + + try { + if (readPermission) { + json.put(READ_PERMISSION, true); + } + if (writePermission) { + json.put(WRITE_PERMISSION, true); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return json; + } + + /* package */ void toParcel(Parcel parcel) { + parcel.writeByte(readPermission ? (byte) 1 : 0); + parcel.writeByte(writePermission ? (byte) 1 : 0); + } + + /* package */ boolean getReadPermission() { + return readPermission; + } + + /* package */ boolean getWritePermission() { + return writePermission; + } + + /* package */ static Permissions createPermissionsFromJSONObject(JSONObject object) { + boolean read = object.optBoolean(READ_PERMISSION, false); + boolean write = object.optBoolean(WRITE_PERMISSION, false); + return new Permissions(read, write); + } + + /* package */ static Permissions createPermissionsFromParcel(Parcel parcel) { + return new Permissions(parcel.readByte() == 1, parcel.readByte() == 1); + } + } + + private static ParseDefaultACLController getDefaultACLController() { + return ParseCorePlugins.getInstance().getDefaultACLController(); + } + + /** + * Sets a default ACL that will be applied to all {@link ParseObject}s when they are created. + * + * @param acl + * The ACL to use as a template for all {@link ParseObject}s created after setDefaultACL + * has been called. This value will be copied and used as a template for the creation of + * new ACLs, so changes to the instance after {@code setDefaultACL(ParseACL, boolean)} + * has been called will not be reflected in new {@link ParseObject}s. + * @param withAccessForCurrentUser + * If {@code true}, the {@code ParseACL} that is applied to newly-created + * {@link ParseObject}s will provide read and write access to the + * {@link ParseUser#getCurrentUser()} at the time of creation. If {@code false}, the + * provided ACL will be used without modification. If acl is {@code null}, this value is + * ignored. + */ + public static void setDefaultACL(ParseACL acl, boolean withAccessForCurrentUser) { + getDefaultACLController().set(acl, withAccessForCurrentUser); + } + + /* package */ static ParseACL getDefaultACL() { + return getDefaultACLController().get(); + } + + // State + private final Map permissionsById = new HashMap<>(); + private boolean shared; + + /** + * A lazy user that hasn't been saved to Parse. + */ + //TODO (grantland): This should be a list for multiple lazy users with read/write permissions. + private ParseUser unresolvedUser; + + /** + * Creates an ACL with no permissions granted. + */ + public ParseACL() { + // do nothing + } + + /** + * Creates a copy of {@code acl}. + * + * @param acl + * The acl to copy. + */ + public ParseACL(ParseACL acl) { + for (String id : acl.permissionsById.keySet()) { + permissionsById.put(id, new Permissions(acl.permissionsById.get(id))); + } + unresolvedUser = acl.unresolvedUser; + if (unresolvedUser != null) { + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); + } + } + + /* package for tests */ ParseACL copy() { + return new ParseACL(this); + } + + boolean isShared() { + return shared; + } + + void setShared(boolean shared) { + this.shared = shared; + } + + // Internally we expose the json object this wraps + /* package */ JSONObject toJSONObject(ParseEncoder objectEncoder) { + JSONObject json = new JSONObject(); + try { + for (String id: permissionsById.keySet()) { + json.put(id, permissionsById.get(id).toJSONObject()); + } + if (unresolvedUser != null) { + Object encoded = objectEncoder.encode(unresolvedUser); + json.put(UNRESOLVED_USER_JSON_KEY, encoded); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return json; + } + + // A helper for creating a ParseACL from the wire. + // We iterate over it rather than just copying to permissionsById so that we + // can ensure it's the right format. + /* package */ static ParseACL createACLFromJSONObject(JSONObject object, ParseDecoder decoder) { + ParseACL acl = new ParseACL(); + + for (String key : ParseJSONUtils.keys(object)) { + if (key.equals(UNRESOLVED_USER_JSON_KEY)) { + JSONObject unresolvedUser; + try { + unresolvedUser = object.getJSONObject(key); + } catch (JSONException e) { + throw new RuntimeException(e); + } + acl.unresolvedUser = (ParseUser) decoder.decode(unresolvedUser); + } else { + try { + Permissions permissions = Permissions.createPermissionsFromJSONObject(object.getJSONObject(key)); + acl.permissionsById.put(key, permissions); + } catch (JSONException e) { + throw new RuntimeException("could not decode ACL: " + e.getMessage()); + } + } + } + return acl; + } + + /** + * Creates an ACL where only the provided user has access. + * + * @param owner + * The only user that can read or write objects governed by this ACL. + */ + public ParseACL(ParseUser owner) { + this(); + setReadAccess(owner, true); + setWriteAccess(owner, true); + } + + /* package for tests */ void resolveUser(ParseUser user) { + if (!isUnresolvedUser(user)) { + return; + } + if (permissionsById.containsKey(UNRESOLVED_KEY)) { + permissionsById.put(user.getObjectId(), permissionsById.get(UNRESOLVED_KEY)); + permissionsById.remove(UNRESOLVED_KEY); + } + unresolvedUser = null; + } + + /* package */ boolean hasUnresolvedUser() { + return unresolvedUser != null; + } + + /* package */ ParseUser getUnresolvedUser() { + return unresolvedUser; + } + + // Helper for setting stuff + private void setPermissionsIfNonEmpty(String userId, boolean readPermission, boolean writePermission) { + if (!(readPermission || writePermission)) { + permissionsById.remove(userId); + } + else { + permissionsById.put(userId, new Permissions(readPermission, writePermission)); + } + } + + /** + * Set whether the public is allowed to read this object. + */ + public void setPublicReadAccess(boolean allowed) { + setReadAccess(PUBLIC_KEY, allowed); + } + + /** + * Get whether the public is allowed to read this object. + */ + public boolean getPublicReadAccess() { + return getReadAccess(PUBLIC_KEY); + } + + /** + * Set whether the public is allowed to write this object. + */ + public void setPublicWriteAccess(boolean allowed) { + setWriteAccess(PUBLIC_KEY, allowed); + } + + /** + * Set whether the public is allowed to write this object. + */ + public boolean getPublicWriteAccess() { + return getWriteAccess(PUBLIC_KEY); + } + + /** + * Set whether the given user id is allowed to read this object. + */ + public void setReadAccess(String userId, boolean allowed) { + if (userId == null) { + throw new IllegalArgumentException("cannot setReadAccess for null userId"); + } + boolean writePermission = getWriteAccess(userId); + setPermissionsIfNonEmpty(userId, allowed, writePermission); + } + + /** + * Get whether the given user id is *explicitly* allowed to read this object. Even if this returns + * {@code false}, the user may still be able to access it if getPublicReadAccess returns + * {@code true} or a role that the user belongs to has read access. + */ + public boolean getReadAccess(String userId) { + if (userId == null) { + throw new IllegalArgumentException("cannot getReadAccess for null userId"); + } + Permissions permissions = permissionsById.get(userId); + return permissions != null && permissions.getReadPermission(); + } + + /** + * Set whether the given user id is allowed to write this object. + */ + public void setWriteAccess(String userId, boolean allowed) { + if (userId == null) { + throw new IllegalArgumentException("cannot setWriteAccess for null userId"); + } + boolean readPermission = getReadAccess(userId); + setPermissionsIfNonEmpty(userId, readPermission, allowed); + } + + /** + * Get whether the given user id is *explicitly* allowed to write this object. Even if this + * returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns + * {@code true} or a role that the user belongs to has write access. + */ + public boolean getWriteAccess(String userId) { + if (userId == null) { + throw new IllegalArgumentException("cannot getWriteAccess for null userId"); + } + Permissions permissions = permissionsById.get(userId); + return permissions != null && permissions.getWritePermission(); + } + + /** + * Set whether the given user is allowed to read this object. + */ + public void setReadAccess(ParseUser user, boolean allowed) { + if (user.getObjectId() == null) { + if (user.isLazy()) { + setUnresolvedReadAccess(user, allowed); + return; + } + throw new IllegalArgumentException("cannot setReadAccess for a user with null id"); + } + setReadAccess(user.getObjectId(), allowed); + } + + private void setUnresolvedReadAccess(ParseUser user, boolean allowed) { + prepareUnresolvedUser(user); + setReadAccess(UNRESOLVED_KEY, allowed); + } + + private void setUnresolvedWriteAccess(ParseUser user, boolean allowed) { + prepareUnresolvedUser(user); + setWriteAccess(UNRESOLVED_KEY, allowed); + } + + private void prepareUnresolvedUser(ParseUser user) { + // Registers a listener for the user so that when it is saved, the + // unresolved ACL will be resolved. + if (!isUnresolvedUser(user)) { + permissionsById.remove(UNRESOLVED_KEY); + unresolvedUser = user; + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); + } + } + + private boolean isUnresolvedUser(ParseUser other) { + // This might be a different instance, but if they have the same local id, assume it's correct. + if (other == null || unresolvedUser == null) return false; + return other == unresolvedUser || (other.getObjectId() == null && + other.getOrCreateLocalId().equals(unresolvedUser.getOrCreateLocalId())); + } + + /** + * Get whether the given user id is *explicitly* allowed to read this object. Even if this returns + * {@code false}, the user may still be able to access it if getPublicReadAccess returns + * {@code true} or a role that the user belongs to has read access. + */ + public boolean getReadAccess(ParseUser user) { + if (isUnresolvedUser(user)) { + return getReadAccess(UNRESOLVED_KEY); + } + if (user.isLazy()) { + return false; + } + if (user.getObjectId() == null) { + throw new IllegalArgumentException("cannot getReadAccess for a user with null id"); + } + return getReadAccess(user.getObjectId()); + } + + /** + * Set whether the given user is allowed to write this object. + */ + public void setWriteAccess(ParseUser user, boolean allowed) { + if (user.getObjectId() == null) { + if (user.isLazy()) { + setUnresolvedWriteAccess(user, allowed); + return; + } + throw new IllegalArgumentException("cannot setWriteAccess for a user with null id"); + } + setWriteAccess(user.getObjectId(), allowed); + } + + /** + * Get whether the given user id is *explicitly* allowed to write this object. Even if this + * returns {@code false}, the user may still be able to write it if getPublicWriteAccess returns + * {@code true} or a role that the user belongs to has write access. + */ + public boolean getWriteAccess(ParseUser user) { + if (isUnresolvedUser(user)) { + return getWriteAccess(UNRESOLVED_KEY); + } + if (user.isLazy()) { + return false; + } + if (user.getObjectId() == null) { + throw new IllegalArgumentException("cannot getWriteAccess for a user with null id"); + } + return getWriteAccess(user.getObjectId()); + } + + /** + * Get whether users belonging to the role with the given roleName are allowed to read this + * object. Even if this returns {@code false}, the role may still be able to read it if a parent + * role has read access. + * + * @param roleName + * The name of the role. + * @return {@code true} if the role has read access. {@code false} otherwise. + */ + public boolean getRoleReadAccess(String roleName) { + return getReadAccess(KEY_ROLE_PREFIX + roleName); + } + + /** + * Set whether users belonging to the role with the given roleName are allowed to read this + * object. + * + * @param roleName + * The name of the role. + * @param allowed + * Whether the given role can read this object. + */ + public void setRoleReadAccess(String roleName, boolean allowed) { + setReadAccess(KEY_ROLE_PREFIX + roleName, allowed); + } + + /** + * Get whether users belonging to the role with the given roleName are allowed to write this + * object. Even if this returns {@code false}, the role may still be able to write it if a parent + * role has write access. + * + * @param roleName + * The name of the role. + * @return {@code true} if the role has write access. {@code false} otherwise. + */ + public boolean getRoleWriteAccess(String roleName) { + return getWriteAccess(KEY_ROLE_PREFIX + roleName); + } + + /** + * Set whether users belonging to the role with the given roleName are allowed to write this + * object. + * + * @param roleName + * The name of the role. + * @param allowed + * Whether the given role can write this object. + */ + public void setRoleWriteAccess(String roleName, boolean allowed) { + setWriteAccess(KEY_ROLE_PREFIX + roleName, allowed); + } + + private static void validateRoleState(ParseRole role) { + if (role == null || role.getObjectId() == null) { + throw new IllegalArgumentException( + "Roles must be saved to the server before they can be used in an ACL."); + } + } + + /** + * Get whether users belonging to the given role are allowed to read this object. Even if this + * returns {@code false}, the role may still be able to read it if a parent role has read access. + * The role must already be saved on the server and its data must have been fetched in order to + * use this method. + * + * @param role + * The role to check for access. + * @return {@code true} if the role has read access. {@code false} otherwise. + */ + public boolean getRoleReadAccess(ParseRole role) { + validateRoleState(role); + return getRoleReadAccess(role.getName()); + } + + /** + * Set whether users belonging to the given role are allowed to read this object. The role must + * already be saved on the server and its data must have been fetched in order to use this method. + * + * @param role + * The role to assign access. + * @param allowed + * Whether the given role can read this object. + */ + public void setRoleReadAccess(ParseRole role, boolean allowed) { + validateRoleState(role); + setRoleReadAccess(role.getName(), allowed); + } + + /** + * Get whether users belonging to the given role are allowed to write this object. Even if this + * returns {@code false}, the role may still be able to write it if a parent role has write + * access. The role must already be saved on the server and its data must have been fetched in + * order to use this method. + * + * @param role + * The role to check for access. + * @return {@code true} if the role has write access. {@code false} otherwise. + */ + public boolean getRoleWriteAccess(ParseRole role) { + validateRoleState(role); + return getRoleWriteAccess(role.getName()); + } + + /** + * Set whether users belonging to the given role are allowed to write this object. The role must + * already be saved on the server and its data must have been fetched in order to use this method. + * + * @param role + * The role to assign access. + * @param allowed + * Whether the given role can write this object. + */ + public void setRoleWriteAccess(ParseRole role, boolean allowed) { + validateRoleState(role); + setRoleWriteAccess(role.getName(), allowed); + } + + private static class UserResolutionListener implements GetCallback { + private final WeakReference parent; + + public UserResolutionListener(ParseACL parent) { + this.parent = new WeakReference<>(parent); + } + + @Override + public void done(ParseObject object, ParseException e) { + // A callback that will resolve the user when it is saved for any + // ACL that is listening to it. + try { + ParseACL parent = this.parent.get(); + if (parent != null) { + parent.resolveUser((ParseUser) object); + } + } finally { + object.unregisterSaveListener(this); + } + } + + } + + /* package for tests */ Map getPermissionsById() { + return permissionsById; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeByte(shared ? (byte) 1 : 0); + dest.writeInt(permissionsById.size()); + Set keys = permissionsById.keySet(); + for (String key : keys) { + dest.writeString(key); + Permissions permissions = permissionsById.get(key); + permissions.toParcel(dest); + } + dest.writeByte(unresolvedUser != null ? (byte) 1 : 0); + if (unresolvedUser != null) { + // Encoder will create a local id for unresolvedUser, so we recognize it after unparcel. + encoder.encode(unresolvedUser, dest); + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseACL createFromParcel(Parcel source) { + return new ParseACL(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseACL[] newArray(int size) { + return new ParseACL[size]; + } + }; + + /* package */ ParseACL(Parcel source, ParseParcelDecoder decoder) { + shared = source.readByte() == 1; + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + Permissions permissions = Permissions.createPermissionsFromParcel(source); + permissionsById.put(key, permissions); + } + if (source.readByte() == 1) { + unresolvedUser = (ParseUser) decoder.decode(source); + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddOperation.java new file mode 100644 index 0000000..fe1959a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddOperation.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An operation that adds a new element to an array field. + */ +/** package */ class ParseAddOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Add"; + + protected final ArrayList objects = new ArrayList<>(); + + public ParseAddOperation(Collection coll) { + objects.addAll(coll); + } + + @Override + public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { + JSONObject output = new JSONObject(); + output.put("__op", OP_NAME); + output.put("objects", objectEncoder.encode(objects)); + return output; + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + if (previous == null) { + return this; + } else if (previous instanceof ParseDeleteOperation) { + return new ParseSetOperation(objects); + } else if (previous instanceof ParseSetOperation) { + Object value = ((ParseSetOperation) previous).getValue(); + if (value instanceof JSONArray) { + ArrayList result = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) value); + result.addAll(objects); + return new ParseSetOperation(new JSONArray(result)); + } else if (value instanceof List) { + ArrayList result = new ArrayList<>((List) value); + result.addAll(objects); + return new ParseSetOperation(result); + } else { + throw new IllegalArgumentException("You can only add an item to a List or JSONArray."); + } + } else if (previous instanceof ParseAddOperation) { + ArrayList result = new ArrayList<>(((ParseAddOperation) previous).objects); + result.addAll(objects); + return new ParseAddOperation(result); + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } + + @Override + public Object apply(Object oldValue, String key) { + if (oldValue == null) { + return objects; + } else if (oldValue instanceof JSONArray) { + ArrayList old = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) oldValue); + @SuppressWarnings("unchecked") + ArrayList newValue = (ArrayList) this.apply(old, key); + return new JSONArray(newValue); + } else if (oldValue instanceof List) { + ArrayList result = new ArrayList<>((List) oldValue); + result.addAll(objects); + return result; + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java new file mode 100644 index 0000000..8f19827 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * An operation that adds a new element to an array field, only if it wasn't already present. + */ +/** package */ class ParseAddUniqueOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "AddUnique"; + + protected final LinkedHashSet objects = new LinkedHashSet<>(); + + public ParseAddUniqueOperation(Collection col) { + objects.addAll(col); + } + + @Override + public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { + JSONObject output = new JSONObject(); + output.put("__op", OP_NAME); + output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); + return output; + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override + @SuppressWarnings("unchecked") + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + if (previous == null) { + return this; + } else if (previous instanceof ParseDeleteOperation) { + return new ParseSetOperation(objects); + } else if (previous instanceof ParseSetOperation) { + Object value = ((ParseSetOperation) previous).getValue(); + if (value instanceof JSONArray || value instanceof List) { + return new ParseSetOperation(this.apply(value, null)); + } else { + throw new IllegalArgumentException("You can only add an item to a List or JSONArray."); + } + } else if (previous instanceof ParseAddUniqueOperation) { + List previousResult = + new ArrayList<>(((ParseAddUniqueOperation) previous).objects); + return new ParseAddUniqueOperation((List) this.apply(previousResult, null)); + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } + + @Override + public Object apply(Object oldValue, String key) { + if (oldValue == null) { + return new ArrayList<>(objects); + } else if (oldValue instanceof JSONArray) { + ArrayList old = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) oldValue); + @SuppressWarnings("unchecked") + ArrayList newValue = (ArrayList) this.apply(old, key); + return new JSONArray(newValue); + } else if (oldValue instanceof List) { + ArrayList result = new ArrayList<>((List) oldValue); + + // Build up a Map of objectIds of the existing ParseObjects in this field. + HashMap existingObjectIds = new HashMap<>(); + for (int i = 0; i < result.size(); i++) { + if (result.get(i) instanceof ParseObject) { + existingObjectIds.put(((ParseObject) result.get(i)).getObjectId(), i); + } + } + + // Iterate over the objects to add. If it already exists in the field, + // remove the old one and add the new one. Otherwise, just add normally. + for (Object obj : objects) { + if (obj instanceof ParseObject) { + String objectId = ((ParseObject) obj).getObjectId(); + if (objectId != null && existingObjectIds.containsKey(objectId)) { + result.set(existingObjectIds.get(objectId), obj); + } else if (!result.contains(obj)) { + result.add(obj); + } + } else { + if (!result.contains(obj)) { + result.add(obj); + } + } + } + return result; + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalytics.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalytics.java new file mode 100644 index 0000000..a06861c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalytics.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Intent; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import bolts.Capture; +import bolts.Continuation; +import bolts.Task; + +/** + * The {@code ParseAnalytics} class provides an interface to Parse's logging and analytics backend. + * Methods will return immediately and cache requests (+ timestamps) to be handled "eventually." + * That is, the request will be sent immediately if possible or the next time a network connection + * is available otherwise. + */ +public class ParseAnalytics { + private static final String TAG = "com.parse.ParseAnalytics"; + + /* package for test */ static ParseAnalyticsController getAnalyticsController() { + return ParseCorePlugins.getInstance().getAnalyticsController(); + } + + /** + * Tracks this application being launched (and if this happened as the result of the user opening + * a push notification, this method sends along information to correlate this open with that + * push). + * + * @param intent + * The {@code Intent} that started an {@code Activity}, if any. Can be null. + * @return A Task that is resolved when the event has been tracked by Parse. + */ + public static Task trackAppOpenedInBackground(Intent intent) { + String pushHashStr = getPushHashFromIntent(intent); + final Capture pushHash = new Capture<>(); + if (pushHashStr != null && pushHashStr.length() > 0) { + synchronized (lruSeenPushes) { + if (lruSeenPushes.containsKey(pushHashStr)) { + return Task.forResult(null); + } else { + lruSeenPushes.put(pushHashStr, true); + pushHash.set(pushHashStr); + } + } + } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + return getAnalyticsController().trackAppOpenedInBackground(pushHash.get(), sessionToken); + } + }); + } + + /** + * @deprecated Please use {@link #trackAppOpenedInBackground(android.content.Intent)} instead. + */ + @Deprecated + public static void trackAppOpened(Intent intent) { + trackAppOpenedInBackground(intent); + } + + /** + * Tracks this application being launched (and if this happened as the result of the user opening + * a push notification, this method sends along information to correlate this open with that + * push). + * + * @param intent + * The {@code Intent} that started an {@code Activity}, if any. Can be null. + * @param callback + * callback.done(e) is called when the event has been tracked by Parse. + */ + public static void trackAppOpenedInBackground(Intent intent, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(trackAppOpenedInBackground(intent), callback); + } + + /** + * @deprecated Please use {@link #trackEventInBackground(String)} instead. + */ + @Deprecated + public static void trackEvent(String name) { + trackEventInBackground(name); + } + + /** + * Tracks the occurrence of a custom event. Parse will store a data point at the time of + * invocation with the given event name. + * + * @param name + * The name of the custom event to report to Parse as having happened. + * @param callback + * callback.done(e) is called when the event has been tracked by Parse. + */ + public static void trackEventInBackground(String name, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(trackEventInBackground(name), callback); + } + + /** + * @deprecated Please use {@link #trackEventInBackground(String, java.util.Map)} instead. + */ + @Deprecated + public static void trackEvent(String name, Map dimensions) { + trackEventInBackground(name, dimensions); + } + + /** + * Tracks the occurrence of a custom event with additional dimensions. Parse will store a data + * point at the time of invocation with the given event name. Dimensions will allow segmentation + * of the occurrences of this custom event. + *

+ * To track a user signup along with additional metadata, consider the following: + *

+   * Map dimensions = new HashMap();
+   * dimensions.put("gender", "m");
+   * dimensions.put("source", "web");
+   * dimensions.put("dayType", "weekend");
+   * ParseAnalytics.trackEvent("signup", dimensions);
+   * 
+ * There is a default limit of 8 dimensions per event tracked. + * + * @param name + * The name of the custom event to report to Parse as having happened. + * @param dimensions + * The dictionary of information by which to segment this event. + * @param callback + * callback.done(e) is called when the event has been tracked by Parse. + */ + public static void trackEventInBackground(String name, Map dimensions, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(trackEventInBackground(name, dimensions), callback); + } + + /** + * Tracks the occurrence of a custom event with additional dimensions. Parse will store a data + * point at the time of invocation with the given event name. Dimensions will allow segmentation + * of the occurrences of this custom event. + *

+ * To track a user signup along with additional metadata, consider the following: + *

+   * Map dimensions = new HashMap();
+   * dimensions.put("gender", "m");
+   * dimensions.put("source", "web");
+   * dimensions.put("dayType", "weekend");
+   * ParseAnalytics.trackEvent("signup", dimensions);
+   * 
+ * There is a default limit of 8 dimensions per event tracked. + * + * @param name + * The name of the custom event to report to Parse as having happened. + * + * @return A Task that is resolved when the event has been tracked by Parse. + */ + public static Task trackEventInBackground(String name) { + return trackEventInBackground(name, (Map) null); + } + + /** + * Tracks the occurrence of a custom event with additional dimensions. Parse will store a data + * point at the time of invocation with the given event name. Dimensions will allow segmentation + * of the occurrences of this custom event. + *

+ * To track a user signup along with additional metadata, consider the following: + *

+   * Map dimensions = new HashMap();
+   * dimensions.put("gender", "m");
+   * dimensions.put("source", "web");
+   * dimensions.put("dayType", "weekend");
+   * ParseAnalytics.trackEvent("signup", dimensions);
+   * 
+ * There is a default limit of 8 dimensions per event tracked. + * + * @param name + * The name of the custom event to report to Parse as having happened. + * @param dimensions + * The dictionary of information by which to segment this event. + * + * @return A Task that is resolved when the event has been tracked by Parse. + */ + public static Task trackEventInBackground(final String name, + Map dimensions) { + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("A name for the custom event must be provided."); + } + final Map dimensionsCopy = dimensions != null + ? Collections.unmodifiableMap(new HashMap<>(dimensions)) + : null; + + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + return getAnalyticsController().trackEventInBackground(name, dimensionsCopy, sessionToken); + } + }); + } + + // Developers have the option to manually track push opens or the app open event can be tracked + // automatically by the ParsePushBroadcastReceiver. To avoid double-counting a push open, we track + // the pushes we've seen locally. We don't need to worry about doing this in any sort of durable + // way because a push can only launch the app once. + private static final Map lruSeenPushes = new LinkedHashMap() { + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 10; + } + }; + + /* package */ static void clear() { + synchronized (lruSeenPushes) { + lruSeenPushes.clear(); + } + } + + /* package for test */ static String getPushHashFromIntent(Intent intent) { + String pushData = null; + if (intent != null && intent.getExtras() != null) { + pushData = intent.getExtras().getString(ParsePushBroadcastReceiver.KEY_PUSH_DATA); + } + if (pushData == null) { + return null; + } + String pushHash = null; + try { + JSONObject payload = new JSONObject(pushData); + pushHash = payload.optString("push_hash"); + } catch (JSONException e) { + PLog.e(TAG, "Failed to parse push data: " + e.getMessage()); + } + return pushHash; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalyticsController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalyticsController.java new file mode 100644 index 0000000..e2bf99c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnalyticsController.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +import java.util.Map; + +import bolts.Task; + +/** package */ class ParseAnalyticsController { + + /* package for test */ ParseEventuallyQueue eventuallyQueue; + + public ParseAnalyticsController(ParseEventuallyQueue eventuallyQueue) { + this.eventuallyQueue = eventuallyQueue; + } + + public Task trackEventInBackground(final String name, + Map dimensions, String sessionToken) { + ParseRESTCommand command = ParseRESTAnalyticsCommand.trackEventCommand(name, dimensions, + sessionToken); + + Task eventuallyTask = eventuallyQueue.enqueueEventuallyAsync(command, null); + return eventuallyTask.makeVoid(); + } + + public Task trackAppOpenedInBackground(String pushHash, String sessionToken) { + ParseRESTCommand command = ParseRESTAnalyticsCommand.trackAppOpenedCommand(pushHash, + sessionToken); + + Task eventuallyTask = eventuallyQueue.enqueueEventuallyAsync(command, null); + return eventuallyTask.makeVoid(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnonymousUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnonymousUtils.java new file mode 100644 index 0000000..fc3482f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAnonymousUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import bolts.Continuation; +import bolts.Task; + +/** + * Provides utility functions for working with Anonymously logged-in users. Anonymous users have + * some unique characteristics: + *
    + *
  • Anonymous users don't need a user name or password.
  • + *
  • Once logged out, an anonymous user cannot be recovered.
  • + *
  • When the current user is anonymous, the following methods can be used to switch to a + * different user or convert the anonymous user into a regular one: + *
      + *
    • signUp converts an anonymous user to a standard user with the given username and password. + * Data associated with the anonymous user is retained.
    • + *
    • logIn switches users without converting the anonymous user. Data associated with the + * anonymous user will be lost.
    • + *
    • Service logIn (e.g. Facebook, Twitter) will attempt to convert the anonymous user into a + * standard user by linking it to the service. If a user already exists that is linked to the + * service, it will instead switch to the existing user.
    • + *
    • Service linking (e.g. Facebook, Twitter) will convert the anonymous user into a standard user + * by linking it to the service.
    • + *
    + *
+ */ +public final class ParseAnonymousUtils { + /* package */ static final String AUTH_TYPE = "anonymous"; + + /** + * Whether the user is logged in anonymously. + * + * @param user + * User to check for anonymity. The user must be logged in on this device. + * @return True if the user is anonymous. False if the user is not the current user or is not + * anonymous. + */ + public static boolean isLinked(ParseUser user) { + return user.isLinked(AUTH_TYPE); + } + + /** + * Creates an anonymous user in the background. + * + * @return A Task that will be resolved when logging in is completed. + */ + public static Task logInInBackground() { + return ParseUser.logInWithInBackground(AUTH_TYPE, getAuthData()); + } + + /** + * Creates an anonymous user in the background. + * + * @param callback + * The callback to execute when anonymous user creation is complete. + */ + public static void logIn(LogInCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(logInInBackground(), callback); + } + + /* package */ static Map getAuthData() { + Map authData = new HashMap<>(); + authData.put("id", UUID.randomUUID().toString()); + return authData; + } + + private ParseAnonymousUtils() { + // do nothing + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAuthenticationManager.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAuthenticationManager.java new file mode 100644 index 0000000..636f2b7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseAuthenticationManager.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class ParseAuthenticationManager { + + private final Object lock = new Object(); + private final Map callbacks = new HashMap<>(); + private final ParseCurrentUserController controller; + + public ParseAuthenticationManager(ParseCurrentUserController controller) { + this.controller = controller; + } + + public void register(final String authType, AuthenticationCallback callback) { + if (authType == null) { + throw new IllegalArgumentException("Invalid authType: " + null); + } + + synchronized (lock) { + if (this.callbacks.containsKey(authType)) { + throw new IllegalStateException("Callback already registered for <" + authType + ">: " + + this.callbacks.get(authType)); + } + this.callbacks.put(authType, callback); + } + + if (ParseAnonymousUtils.AUTH_TYPE.equals(authType)) { + // There's nothing to synchronize + return; + } + + // Synchronize the current user with the auth callback. + controller.getAsync(false).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser user = task.getResult(); + if (user != null) { + return user.synchronizeAuthDataAsync(authType); + } + return null; + } + }); + } + + public Task restoreAuthenticationAsync(String authType, final Map authData) { + final AuthenticationCallback callback; + synchronized (lock) { + callback = this.callbacks.get(authType); + } + if (callback == null) { + return Task.forResult(true); + } + return Task.call(new Callable() { + @Override + public Boolean call() throws Exception { + return callback.onRestore(authData); + } + }, ParseExecutors.io()); + } + + public Task deauthenticateAsync(String authType) { + final AuthenticationCallback callback; + synchronized (lock) { + callback = this.callbacks.get(authType); + } + if (callback != null) { + return Task.call(new Callable() { + @Override + public Void call() throws Exception { + callback.onRestore(null); + return null; + } + }, ParseExecutors.io()); + } + return Task.forResult(null); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseByteArrayHttpBody.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseByteArrayHttpBody.java new file mode 100644 index 0000000..a009b0b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseByteArrayHttpBody.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpBody; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +/** package */ class ParseByteArrayHttpBody extends ParseHttpBody { + /* package */ final byte[] content; + /* package */ final InputStream contentInputStream; + + public ParseByteArrayHttpBody(String content, String contentType) + throws UnsupportedEncodingException { + this(content.getBytes("UTF-8"), contentType); + } + + public ParseByteArrayHttpBody(byte[] content, String contentType) { + super(contentType, content.length); + this.content = content; + this.contentInputStream = new ByteArrayInputStream(content); + } + + @Override + public InputStream getContent() { + return contentInputStream; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("Output stream may not be null"); + } + + out.write(content); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback1.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback1.java new file mode 100644 index 0000000..61c9a68 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback1.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code ParseCallback} is used to do something after a background task completes. End users will + * use a specific subclass of {@code ParseCallback}. + */ +/** package */ interface ParseCallback1 { + /** + * {@code done(t)} must be overridden when you are doing a background operation. It is called + * when the background operation completes. + *

+ * If the operation is successful, {@code t} will be {@code null}. + *

+ * If the operation was unsuccessful, {@code t} will contain information about the operation + * failure. + * + * @param t + * Generally an {@link Throwable} that was thrown by the operation, if there was any. + */ + void done(T t); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback2.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback2.java new file mode 100644 index 0000000..9b14207 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCallback2.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code ParseCallback} is used to do something after a background task completes. End users will + * use a specific subclass of {@code ParseCallback}. + */ +/** package */ interface ParseCallback2 { + /** + * {@code done(t1, t2)} must be overridden when you are doing a background operation. It is called + * when the background operation completes. + *

+ * If the operation is successful, {@code t1} will contain the results and {@code t2} will be + * {@code null}. + *

+ * If the operation was unsuccessful, {@code t1} will be {@code null} and {@code t2} will contain + * information about the operation failure. + * + * @param t1 + * Generally the results of the operation. + * @param t2 + * Generally an {@link Throwable} that was thrown by the operation, if there was any. + */ + void done(T1 t1, T2 t2); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseClassName.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseClassName.java new file mode 100644 index 0000000..29c3388 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseClassName.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Associates a class name for a subclass of ParseObject. + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface ParseClassName { + /** + * @return The Parse class name associated with the ParseObject subclass. + */ + String value(); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloud.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloud.java new file mode 100644 index 0000000..f1eba32 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloud.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; +import java.util.Map; + +import bolts.Continuation; +import bolts.Task; + +/** + * The ParseCloud class defines provides methods for interacting with Parse Cloud Functions. A Cloud + * Function can be called with {@link #callFunctionInBackground(String, Map, FunctionCallback)} + * using a {@link FunctionCallback}. For example, this sample code calls the "validateGame" Cloud + * Function and calls processResponse if the call succeeded and handleError if it failed. + * + *

+ * ParseCloud.callFunctionInBackground("validateGame", parameters, new FunctionCallback() {
+ *      public void done(Object object, ParseException e) {
+ *        if (e == null) {
+ *          processResponse(object);
+ *        } else {
+ *          handleError();
+ *        }
+ *      }
+ * }
+ * 
+ *
+ * Using the callback methods is usually preferred because the network operation will not block the
+ * calling thread. However, in some cases it may be easier to use the
+ * {@link #callFunction(String, Map)} call which do block the calling thread. For example, if your
+ * application has already spawned a background task to perform work, that background task could use
+ * the blocking calls and avoid the code complexity of callbacks.
+ */
+public final class ParseCloud {
+
+  /* package for test */ static ParseCloudCodeController getCloudCodeController() {
+    return ParseCorePlugins.getInstance().getCloudCodeController();
+  }
+
+  /**
+   * Calls a cloud function in the background.
+   *
+   * @param name
+   *          The cloud function to call.
+   * @param params
+   *          The parameters to send to the cloud function. This map can contain anything that could
+   *          be placed in a ParseObject except for ParseObjects themselves.
+   *
+   * @return A Task that will be resolved when the cloud function has returned.
+   */
+  public static  Task callFunctionInBackground(final String name,
+      final Map params) {
+    return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() {
+      @Override
+      public Task then(Task task) throws Exception {
+        String sessionToken = task.getResult();
+        return getCloudCodeController().callFunctionInBackground(name, params, sessionToken);
+      }
+    });
+  }
+
+  /**
+   * Calls a cloud function.
+   *
+   * @param name
+   *          The cloud function to call.
+   * @param params
+   *          The parameters to send to the cloud function. This map can contain anything that could
+   *          be placed in a ParseObject except for ParseObjects themselves.
+   * @return The result of the cloud call. Result may be a @{link Map}< {@link String}, ?>,
+   *         {@link ParseObject}, {@link List}<?>, or any type that can be set as a field in a
+   *         ParseObject.
+   * @throws ParseException
+   */
+  public static  T callFunction(String name, Map params) throws ParseException {
+    return ParseTaskUtils.wait(ParseCloud.callFunctionInBackground(name, params));
+  }
+
+  /**
+   * Calls a cloud function in the background.
+   *
+   * @param name
+   *          The cloud function to call.
+   * @param params
+   *          The parameters to send to the cloud function. This map can contain anything that could
+   *          be placed in a ParseObject except for ParseObjects themselves.
+   * @param callback
+   *          The callback that will be called when the cloud function has returned.
+   */
+  public static  void callFunctionInBackground(String name, Map params,
+      FunctionCallback callback) {
+    ParseTaskUtils.callbackOnMainThreadAsync(
+        ParseCloud.callFunctionInBackground(name, params),
+        callback);
+  }
+
+  private ParseCloud() {
+    // do nothing
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloudCodeController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloudCodeController.java
new file mode 100644
index 0000000..3316b86
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCloudCodeController.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import org.json.JSONObject;
+
+import java.util.Map;
+
+import bolts.Continuation;
+import bolts.Task;
+
+/** package */ class ParseCloudCodeController {
+
+  /* package for test */ final ParseHttpClient restClient;
+
+  public ParseCloudCodeController(ParseHttpClient restClient) {
+    this.restClient = restClient;
+  }
+
+  public  Task callFunctionInBackground(final String name,
+      final Map params, String sessionToken) {
+    ParseRESTCommand command = ParseRESTCloudCommand.callFunctionCommand(
+        name,
+        params,
+        sessionToken);
+    return command.executeAsync(restClient).onSuccess(new Continuation() {
+      @Override
+      public T then(Task task) throws Exception {
+        @SuppressWarnings("unchecked")
+        T result = (T) convertCloudResponse(task.getResult());
+        return result;
+      }
+    });
+  }
+
+  /*
+   * Decodes any Parse data types in the result of the cloud function call.
+   */
+  /* package for test */ Object convertCloudResponse(Object result) {
+    if (result instanceof JSONObject) {
+      JSONObject jsonResult = (JSONObject)result;
+      result = jsonResult.opt("result");
+    }
+
+    ParseDecoder decoder = ParseDecoder.get();
+    Object finalResult = decoder.decode(result);
+    if (finalResult != null) {
+      return finalResult;
+    }
+
+    return result;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCommandCache.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCommandCache.java
new file mode 100644
index 0000000..4cdf5d3
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCommandCache.java
@@ -0,0 +1,688 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.Callable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import bolts.Capture;
+import bolts.Continuation;
+import bolts.Task;
+import bolts.TaskCompletionSource;
+
+/**
+ * ParseCommandCache manages an on-disk cache of commands to be executed, and a thread with a
+ * standard run loop that executes the commands. There should only ever be one instance of this
+ * class, because multiple instances would be running separate threads trying to read and execute
+ * the same commands.
+ */
+/** package */ class ParseCommandCache extends ParseEventuallyQueue {
+  private static final String TAG = "com.parse.ParseCommandCache";
+
+  private static int filenameCounter = 0; // Appended to temp file names so we know their creation
+                                          // order.
+
+  // Lock guards access to the file system and all of the instance variables above. It is static so
+  // that if somehow there are two instances of ParseCommandCache, they won't step on each others'
+  // toes while using the file system. A thread with lock should *not* try to get runningLock, under
+  // penalty of deadlock. Only the run loop (runLoop) thread should ever wait on this lock. Other
+  // threads should notify on this lock whenever the run loop should wake up and try to execute more
+  // commands.
+  private static final Object lock = new Object();
+
+  private static File getCacheDir() {
+    // Construct the path to the cache directory.
+    File cacheDir = new File(Parse.getParseDir(), "CommandCache");
+    cacheDir.mkdirs();
+
+    return cacheDir;
+  }
+
+  public static int getPendingCount() {
+    synchronized (lock) {
+      String[] files = getCacheDir().list();
+      return files == null ? 0 : files.length;
+    }
+  }
+
+  private File cachePath; // Where the cache is stored on disk.
+  private int timeoutMaxRetries = 5; // Don't retry more than 5 times before assuming disconnection.
+  private double timeoutRetryWaitSeconds = 600.0f; // Wait 10 minutes before retrying after network
+                                                   // timeout.
+  private int maxCacheSizeBytes = 10 * 1024 * 1024; // Don't consume more than N bytes of storage.
+
+  private boolean shouldStop; // Should the run loop thread processing the disk cache continue?
+  private boolean unprocessedCommandsExist; // Has a command been added which hasn't yet been
+                                            // processed by the run loop?
+
+  // Map of filename to TaskCompletionSource, for all commands that are in the queue from this run
+  // of the program. This is necessary so that the original objects can be notified after their
+  // saves complete.
+  private HashMap> pendingTasks = new HashMap<>();
+
+  private boolean running; // Is the run loop executing commands from the disk cache running?
+
+  // Guards access to running. Gets a broadcast whenever running changes. A thread should only wait
+  // on runningLock if it's sure the value of running is going to change. Only the run loop
+  // (runLoop) thread should ever notify on runningLock. It's perfectly fine for a thread that has
+  // runningLock to then also try to acquire the other lock.
+  private final Object runningLock;
+
+  private Logger log; // Why is there a custom logger? To prevent Mockito deadlock!
+
+  private final ParseHttpClient httpClient;
+
+  ConnectivityNotifier notifier;
+  ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() {
+    @Override
+    public void networkConnectivityStatusChanged(Context context, Intent intent) {
+      final boolean connectionLost =
+          intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+      final boolean isConnected = ConnectivityNotifier.isConnected(context);
+
+      /*
+       Hack to avoid blocking the UI thread with disk I/O
+
+       setConnected uses the same lock we use for synchronizing disk I/O, so there's a possibility
+       that we can block the UI thread on disk I/O, so we're going to bump the lock usage to a
+       different thread.
+
+       TODO(grantland): Convert to TaskQueue, similar to ParsePinningEventuallyQueue
+        */
+      Task.call(new Callable() {
+        @Override
+        public Void call() throws Exception {
+          if (connectionLost) {
+            setConnected(false);
+          } else {
+            setConnected(isConnected);
+          }
+          return null;
+        }
+      }, ParseExecutors.io());
+    }
+  };
+
+  public ParseCommandCache(Context context, ParseHttpClient client) {
+    setConnected(false);
+
+    shouldStop = false;
+    running = false;
+
+    runningLock = new Object();
+    httpClient = client;
+
+    log = Logger.getLogger(TAG);
+
+    cachePath = getCacheDir();
+
+    if (!Parse.hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) {
+      // The command cache only works if the user has granted us permission to monitor the network.
+      return;
+    }
+
+    setConnected(ConnectivityNotifier.isConnected(context));
+    notifier = ConnectivityNotifier.getNotifier(context);
+    notifier.addListener(listener);
+    
+    resume();
+  }
+
+  @Override
+  public void onDestroy() {
+    //TODO (grantland): pause #6484855
+
+    notifier.removeListener(listener);
+  }
+
+  // Set the maximum number of times to retry before assuming disconnection.
+  @SuppressWarnings("unused")
+  public void setTimeoutMaxRetries(int tries) {
+    synchronized (lock) {
+      timeoutMaxRetries = tries;
+    }
+  }
+
+  // Sets the amount of time to wait before retrying after network timeout.
+  public void setTimeoutRetryWaitSeconds(double seconds) {
+    synchronized (lock) {
+      timeoutRetryWaitSeconds = seconds;
+    }
+  }
+
+  // Sets the maximum amount of storage space this cache can consume.
+  public void setMaxCacheSizeBytes(int bytes) {
+    synchronized (lock) {
+      maxCacheSizeBytes = bytes;
+    }
+  }
+
+  // Starts the run loop thread running.
+  public void resume() {
+    synchronized (runningLock) {
+      if (!running) {
+        new Thread("ParseCommandCache.runLoop()") {
+          @Override
+          public void run() {
+            runLoop();
+          }
+        }.start();
+        try {
+          runningLock.wait();
+        } catch (InterruptedException e) {
+          // Someone told this thread to stop.
+          synchronized (lock) {
+            shouldStop = true;
+            lock.notifyAll();
+          }
+        }
+      }
+    }
+  }
+
+  // Stops the run loop thread from processing commands until resume is called.
+  // When this function returns, the run loop has stopped.
+  public void pause() {
+    synchronized (runningLock) {
+      if (running) {
+        synchronized (lock) {
+          shouldStop = true;
+          lock.notifyAll();
+        }
+      }
+      while (running) {
+        try {
+          runningLock.wait();
+        } catch (InterruptedException e) {
+          // Someone told this thread to stop while it was already waiting to
+          // finish...
+          // Ignore them and continue waiting.
+        }
+      }
+    }
+  }
+
+  /**
+   * Removes a file from the file system and any internal caches.
+   */
+  private void removeFile(File file) {
+    synchronized (lock) {
+      // Remove the data in memory for this command.
+      pendingTasks.remove(file);
+
+      // Release all the localIds referenced by the command.
+      // Read one command from the cache.
+      JSONObject json;
+      try {
+        json = ParseFileUtils.readFileToJSONObject(file);
+
+        ParseRESTCommand command = commandFromJSON(json);
+        command.releaseLocalIds();
+      } catch (Exception e) {
+        // Well, we did our best. We'll just have to leak a localId.
+      }
+
+      // Delete the command file itself.
+      ParseFileUtils.deleteQuietly(file);
+    }
+  }
+
+  /**
+   * Makes this command cache forget all the state it keeps during a single run of the app. This is
+   * only for testing purposes.
+   */
+  void simulateReboot() {
+    synchronized (lock) {
+      pendingTasks.clear();
+    }
+  }
+
+  /**
+   * Fakes an object update notification for use in tests. This is used by saveEventually to make it
+   * look like test code has updated an object through the command cache even if it actually
+   * avoided executing update by determining the object wasn't dirty.
+   */
+  void fakeObjectUpdate() {
+    notifyTestHelper(TestHelper.COMMAND_ENQUEUED);
+    notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL);
+    notifyTestHelper(TestHelper.OBJECT_UPDATED);
+  }
+
+  @Override
+  public Task enqueueEventuallyAsync(ParseRESTCommand command,
+      ParseObject object) {
+    return enqueueEventuallyAsync(command, false, object);
+  }
+
+  /**
+   * Attempts to run the given command and any pending commands. Adds the command to the pending set
+   * if it can't be run yet.
+   *
+   * @param command
+   *          - The command to run.
+   * @param preferOldest
+   *          - When the disk is full, if preferOldest, drop new commands. Otherwise, the oldest
+   *          commands will be deleted to make room.
+   * @param object
+   *          - See runEventually.
+   */
+  private Task enqueueEventuallyAsync(ParseRESTCommand command, boolean preferOldest,
+      ParseObject object) {
+    Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE);
+    TaskCompletionSource tcs = new TaskCompletionSource<>();
+    byte[] json;
+    try {
+      // If this object doesn't have an objectId yet, store the localId so we can remap it to the
+      // objectId after the save completes.
+      if (object != null && object.getObjectId() == null) {
+        command.setLocalId(object.getOrCreateLocalId());
+      }
+      JSONObject jsonObject = command.toJSONObject();
+      json = jsonObject.toString().getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
+        log.log(Level.WARNING, "UTF-8 isn't supported.  This shouldn't happen.", e);
+      }
+      notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED);
+      return Task.forResult(null);
+    }
+
+    // If this object by itself is larger than the full disk cache, then don't
+    // even bother trying.
+    if (json.length > maxCacheSizeBytes) {
+      if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
+        log.warning("Unable to save command for later because it's too big.");
+      }
+      notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED);
+      return Task.forResult(null);
+    }
+
+    synchronized (lock) {
+      try {
+        // Is there enough free storage space?
+        String[] fileNames = cachePath.list();
+        if (fileNames != null) {
+          Arrays.sort(fileNames);
+          int size = 0;
+          for (String fileName : fileNames) {
+            File file = new File(cachePath, fileName);
+            // Should be safe to convert long to int, because we don't allow
+            // files larger than 2GB.
+            size += (int) file.length();
+          }
+          size += json.length;
+          if (size > maxCacheSizeBytes) {
+            if (preferOldest) {
+              if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
+                log.warning("Unable to save command for later because storage is full.");
+              }
+              return Task.forResult(null);
+            } else {
+              if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
+                log.warning("Deleting old commands to make room in command cache.");
+              }
+              int indexToDelete = 0;
+              while (size > maxCacheSizeBytes && indexToDelete < fileNames.length) {
+                File file = new File(cachePath, fileNames[indexToDelete++]);
+                size -= (int) file.length();
+                removeFile(file);
+              }
+            }
+          }
+        }
+
+        // Get the current time to store in the filename, so that we process them in order.
+        String prefix1 = Long.toHexString(System.currentTimeMillis());
+        if (prefix1.length() < 16) {
+          char[] zeroes = new char[16 - prefix1.length()];
+          Arrays.fill(zeroes, '0');
+          prefix1 = new String(zeroes) + prefix1;
+        }
+
+        // Then add another incrementing number in case we enqueue items faster than the system's
+        // time granularity.
+        String prefix2 = Integer.toHexString(filenameCounter++);
+        if (prefix2.length() < 8) {
+          char[] zeroes = new char[8 - prefix2.length()];
+          Arrays.fill(zeroes, '0');
+          prefix2 = new String(zeroes) + prefix2;
+        }
+
+        String prefix = "CachedCommand_" + prefix1 + "_" + prefix2 + "_";
+
+        // Get a unique filename to store this command in.
+        File path = File.createTempFile(prefix, "", cachePath);
+
+        // Write the command to that file.
+        pendingTasks.put(path, tcs);
+        command.retainLocalIds();
+        ParseFileUtils.writeByteArrayToFile(path, json);
+
+        notifyTestHelper(TestHelper.COMMAND_ENQUEUED);
+
+        unprocessedCommandsExist = true;
+      } catch (IOException e) {
+        if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) {
+          log.log(Level.WARNING, "Unable to save command for later.", e);
+        }
+      } finally {
+        lock.notifyAll();
+      }
+    }
+    return tcs.getTask();
+  }
+
+  /**
+   * Returns the number of commands currently in the set waiting to be run.
+   */
+  @Override
+  public int pendingCount() {
+    return getPendingCount();
+  }
+
+  /**
+   * Gets rid of all pending commands.
+   */
+  public void clear() {
+    synchronized (lock) {
+      File[] files = cachePath.listFiles();
+      if (files == null) {
+        return;
+      }
+      for (File file : files) {
+        removeFile(file);
+      }
+      pendingTasks.clear();
+    }
+  }
+
+  /**
+   * Manually sets the network connection status.
+   */
+  public void setConnected(boolean connected) {
+    synchronized (lock) {
+      if (isConnected() != connected) {
+        if (connected) {
+          lock.notifyAll();
+        }
+      }
+      super.setConnected(connected);
+    }
+  }
+
+  /**
+   * This is kind of like ParseTaskUtils.wait(), except that it gives up the CommandCache's lock
+   * while the task is running, and reclaims it before returning.
+   */
+  private  T waitForTaskWithoutLock(Task task) throws ParseException {
+    synchronized (lock) {
+      final Capture finished = new Capture<>(false);
+      task.continueWith(new Continuation() {
+        @Override
+        public Void then(Task task) throws Exception {
+          finished.set(true);
+          synchronized(lock) {
+            lock.notifyAll();
+          }
+          return null;
+        }
+      }, Task.BACKGROUND_EXECUTOR);
+      while (!finished.get()) {
+        try {
+          lock.wait();
+        } catch (InterruptedException ie) {
+          shouldStop = true;
+        }
+      }
+      return ParseTaskUtils.wait(task);  // Just to get the return value and maybe rethrow.
+    }
+  }
+  
+  /**
+   * Attempts to run every command in the disk queue in order, synchronously. If there is no network
+   * connection, returns immediately without doing anything. If there is supposedly a connection,
+   * but parse can't be reached, waits timeoutRetryWaitSeconds before retrying up to
+   * retriesRemaining times. Blocks until either there's a connection, or the retries are exhausted.
+   * If any command fails, just deletes it and moves on to the next one.
+   */
+  private void maybeRunAllCommandsNow(int retriesRemaining) {
+    synchronized (lock) {
+      unprocessedCommandsExist = false;
+      
+      if (!isConnected()) {
+        // There's no way to do work when there's no network connection.
+        return;
+      }
+
+      String[] fileNames = cachePath.list();
+      if (fileNames == null || fileNames.length == 0) {
+        return;
+      }
+      Arrays.sort(fileNames);
+      for (String fileName : fileNames) {
+        final File file = new File(cachePath, fileName);
+
+        // Read one command from the cache.
+        JSONObject json;
+        try {
+          json = ParseFileUtils.readFileToJSONObject(file);
+        } catch (FileNotFoundException e) {
+          // This shouldn't really be possible.
+          if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+            log.log(Level.SEVERE, "File disappeared from cache while being read.", e);
+          }
+          continue;
+        } catch (IOException e) {
+          if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+            log.log(Level.SEVERE, "Unable to read contents of file in cache.", e);
+          }
+          removeFile(file);
+          continue;
+        } catch (JSONException e) {
+          if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+            log.log(Level.SEVERE, "Error parsing JSON found in cache.", e);
+          }
+          removeFile(file);
+          continue;
+        }
+
+        // Convert the command from a string.
+        final ParseRESTCommand command;
+        final TaskCompletionSource tcs =
+            pendingTasks.containsKey(file) ? pendingTasks.get(file) : null;
+
+        try {
+          command = commandFromJSON(json);
+        } catch (JSONException e) {
+          if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+            log.log(Level.SEVERE, "Unable to create ParseCommand from JSON.", e);
+          }
+          removeFile(file);
+          continue;
+        }
+
+        try {
+          Task commandTask;
+          if (command == null) {
+            commandTask = Task.forResult(null);
+            if (tcs != null) {
+              tcs.setResult(null);
+            }
+            notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED);
+          } else {
+            commandTask = command.executeAsync(httpClient).continueWithTask(new Continuation>() {
+              @Override
+              public Task then(Task task) throws Exception {
+                String localId = command.getLocalId();
+                Exception error = task.getError();
+                if (error != null) {
+                  if (error instanceof ParseException
+                      && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) {
+                    // do nothing
+                  } else {
+                    if (tcs != null) {
+                      tcs.setError(error);
+                    }
+                  }
+                  return task;
+                }
+
+                JSONObject json = task.getResult();
+                if (tcs != null) {
+                  tcs.setResult(json);
+                } else if (localId != null) {
+                  // If this command created a new objectId, add it to the map.
+                  String objectId = json.optString("objectId", null);
+                  if (objectId != null) {
+                    ParseCorePlugins.getInstance()
+                        .getLocalIdManager().setObjectId(localId, objectId);
+                  }
+                }
+                return task;
+              }
+            });
+          }
+
+          waitForTaskWithoutLock(commandTask);
+          if (tcs != null) {
+            waitForTaskWithoutLock(tcs.getTask());
+          }
+          
+          // The command succeeded. Remove it from the cache.
+          removeFile(file);
+          notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL);
+        } catch (ParseException e) {
+          if (e.getCode() == ParseException.CONNECTION_FAILED) {
+            if (retriesRemaining > 0) {
+              // Reachability says we have a network connection, but we can't actually contact
+              // Parse. Wait N minutes, or until we get signaled again before doing anything else.
+              if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
+                log.info("Network timeout in command cache. Waiting for " + timeoutRetryWaitSeconds
+                    + " seconds and then retrying " + retriesRemaining + " times.");
+              }
+              long currentTime = System.currentTimeMillis();
+              long waitUntil = currentTime + (long) (timeoutRetryWaitSeconds * 1000);
+              while (currentTime < waitUntil) {
+                // We haven't waited long enough, but if we lost the connection,
+                // or should stop, just quit.
+                if (!isConnected() || shouldStop) {
+                  if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
+                    log.info("Aborting wait because runEventually thread should stop.");
+                  }
+                  return;
+                }
+                try {
+                  lock.wait(waitUntil - currentTime);
+                } catch (InterruptedException ie) {
+                  shouldStop = true;
+                }
+                currentTime = System.currentTimeMillis();
+                if (currentTime < (waitUntil - (long) (timeoutRetryWaitSeconds * 1000))) {
+                  // This situation should be impossible, so it must mean the clock changed.
+                  currentTime = (waitUntil - (long) (timeoutRetryWaitSeconds * 1000));
+                }
+              }
+              maybeRunAllCommandsNow(retriesRemaining - 1);
+            } else {
+              setConnected(false);
+
+              notifyTestHelper(TestHelper.NETWORK_DOWN);
+            }
+          } else {
+            if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+              log.log(Level.SEVERE, "Failed to run command.", e);
+            }
+            // Delete the command from the cache, even though it failed.
+            // Otherwise, we'll just keep trying it forever.
+            removeFile(file);
+            notifyTestHelper(TestHelper.COMMAND_FAILED, e);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * The main function of the run loop thread. This function basically loops forever (unless pause
+   * is called). On each iteration, if it hasn't been told to stop, it calls maybeRunAllCommandsNow
+   * to try to execute everything queued up on disk. Then it waits until it gets signaled again by
+   * lock.notify(). Usually that happens as a result of either (1) Parse being initialized, (2)
+   * runEventually being called, or (3) the OS notifying that the network connection has been
+   * re-established.
+   */
+  private void runLoop() {
+    if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
+      log.info("Parse command cache has started processing queued commands.");
+    }
+    // Make sure we marked as running.
+    synchronized (runningLock) {
+      if (running) {
+        // Don't run this thread more than once.
+        return;
+      } else {
+        running = true;
+        runningLock.notifyAll();
+      }
+    }
+
+    boolean shouldRun;
+    synchronized (lock) {
+      shouldRun = !(shouldStop || Thread.interrupted());
+    }
+    while (shouldRun) {
+      synchronized (lock) {
+        try {
+          maybeRunAllCommandsNow(timeoutMaxRetries);
+          if (!shouldStop) {
+            try {
+              /*
+               * If an unprocessed command was added, avoid waiting because we want
+               * maybeRunAllCommandsNow to run at least once to potentially process that command.
+               */
+              if (!unprocessedCommandsExist) {
+                lock.wait();
+              }
+            } catch (InterruptedException e) {
+              shouldStop = true;
+            }
+          }
+        } catch (Exception e) {
+          if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) {
+            log.log(Level.SEVERE, "saveEventually thread had an error.", e);
+          }
+        } finally {
+          shouldRun = !shouldStop;
+        }
+      }
+    }
+
+    synchronized (runningLock) {
+      running = false;
+      runningLock.notifyAll();
+    }
+    if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) {
+      log.info("saveEventually thread has stopped processing commands.");
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfig.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfig.java
new file mode 100644
index 0000000..3c7f035
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfig.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import bolts.Continuation;
+import bolts.Task;
+
+/**
+ * The {@code ParseConfig} is a local representation of configuration data that can be set from the
+ * Parse dashboard.
+ */
+public class ParseConfig {
+  /* package for tests */ static final TaskQueue taskQueue = new TaskQueue();
+
+  /* package for tests */ final Map params;
+
+  /* package for tests */ static ParseConfigController getConfigController() {
+    return ParseCorePlugins.getInstance().getConfigController();
+  }
+
+  /**
+   * Retrieves the most recently-fetched configuration object, either from memory or
+   * disk if necessary.
+   *
+   * @return The most recently-fetched {@code ParseConfig} if it exists, else an empty
+   *          {@code ParseConfig}
+   */
+  public static ParseConfig getCurrentConfig() {
+    try {
+      return ParseTaskUtils.wait(getConfigController().getCurrentConfigController()
+          .getCurrentConfigAsync()
+      );
+    } catch (ParseException e) {
+      // In order to have backward compatibility, we swallow the exception silently.
+      return new ParseConfig();
+    }
+  }
+
+  /**
+   * Fetches a new configuration object from the server.
+   *
+   * @throws ParseException
+   *           Throws an exception if the server is inaccessible.
+   * @return The {@code ParseConfig} that was fetched.
+   */
+  public static ParseConfig get() throws ParseException {
+    return ParseTaskUtils.wait(getInBackground());
+  }
+
+  /**
+   * Fetches a new configuration object from the server in a background thread. This is preferable
+   * to using {@link #get()}, unless your code is already running from a background thread.
+   *
+   * @param callback
+   *          callback.done(config, e) is called when the fetch completes.
+   */
+  public static void getInBackground(ConfigCallback callback) {
+    ParseTaskUtils.callbackOnMainThreadAsync(getInBackground(), callback);
+  }
+
+  /**
+   * Fetches a new configuration object from the server in a background thread. This is preferable
+   * to using {@link #get()}, unless your code is already running from a background thread.
+   *
+   * @return A Task that is resolved when the fetch completes.
+   */
+  public static Task getInBackground() {
+    return taskQueue.enqueue(new Continuation>() {
+      @Override
+      public Task then(Task toAwait) throws Exception {
+        return getAsync(toAwait);
+      }
+    });
+  }
+
+  private static Task getAsync(final Task toAwait) {
+    return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() {
+      @Override
+      public Task then(Task task) throws Exception {
+        final String sessionToken = task.getResult();
+        return toAwait.continueWithTask(new Continuation>() {
+          @Override
+          public Task then(Task task) throws Exception {
+            return getConfigController().getAsync(sessionToken);
+          }
+        });
+      }
+    });
+  }
+
+  @SuppressWarnings("unchecked")
+  /* package */ static ParseConfig decode(JSONObject json, ParseDecoder decoder) {
+    Map decodedObject = (Map) decoder.decode(json);
+    Map decodedParams = (Map) decodedObject.get("params");
+    if (decodedParams == null) {
+      throw new RuntimeException("Object did not contain the 'params' key.");
+    }
+    return new ParseConfig(decodedParams);
+  }
+
+  /* package */ ParseConfig(Map params) {
+    this.params = Collections.unmodifiableMap(params);
+  }
+
+  /* package */ ParseConfig() {
+    params = Collections.unmodifiableMap(new HashMap());
+  }
+
+  /* package */ Map getParams() {
+    return Collections.unmodifiableMap(new HashMap<>(params));
+  }
+
+  /**
+   * Access a value. In most cases it is more convenient to use a helper function such as
+   * {@link #getString} or {@link #getInt}.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key.
+   */
+  public Object get(String key) {
+    return get(key, null);
+  }
+
+  /**
+   * Access a value, returning a default value if the key doesn't exist. In most cases it is more
+   * convenient to use a helper function such as {@link #getString} or {@link #getInt}.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present in the configuration object.
+   * @return The default value if there is no such key.
+   */
+  public Object get(String key, Object defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == JSONObject.NULL) {
+      return null;
+    }
+    return params.get(key);
+  }
+
+  /**
+   * Access a {@code boolean} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns false if there is no such key or if it is not a {@code boolean}.
+   */
+  public boolean getBoolean(String key) {
+    return getBoolean(key, false);
+  }
+
+  /**
+   * Access a {@code boolean} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@code boolean}.
+   */
+  public boolean getBoolean(String key, boolean defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    return (value instanceof Boolean) ? (Boolean) value : defaultValue;
+  }
+
+  /**
+   * Access a {@link Date} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it is not a {@link Date}.
+   */
+  public Date getDate(String key) {
+    return getDate(key, null);
+  }
+
+  /**
+   * Access a {@link Date} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link Date}.
+   */
+  public Date getDate(String key, Date defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    return (value instanceof Date) ? (Date) value : defaultValue;
+  }
+
+  /**
+   * Access a {@code double} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns 0 if there is no such key or if it is not a number.
+   */
+  public double getDouble(String key) {
+    return getDouble(key, 0.0);
+  }
+
+  /**
+   * Access a {@code double} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a number.
+   */
+  public double getDouble(String key, double defaultValue) {
+    Number number = getNumber(key);
+    return number != null ? number.doubleValue() : defaultValue;
+  }
+
+  /**
+   * Access an {@code int} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns 0 if there is no such key or if it is not a number.
+   */
+  public int getInt(String key) {
+    return getInt(key, 0);
+  }
+
+  /**
+   * Access an {@code int} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a number.
+   */
+  public int getInt(String key, int defaultValue) {
+    Number number = getNumber(key);
+    return number != null ? number.intValue() : defaultValue;
+  }
+
+  /**
+   * Access a {@link JSONArray} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it is not a {@link JSONArray}.
+   */
+  public JSONArray getJSONArray(String key) {
+    return getJSONArray(key, null);
+  }
+
+  /**
+   * Access a {@link JSONArray} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link JSONArray}.
+   */
+  public JSONArray getJSONArray(String key, JSONArray defaultValue) {
+    List list = getList(key);
+    Object encoded = (list != null) ? PointerEncoder.get().encode(list) : null;
+    //TODO(mengyan) There are actually two cases, getList(key) will return null
+    // case 1: key not exist, in this situation, we should return JSONArray defaultValue
+    // case 2: key exist but value is Json.NULL, in this situation, we should return null
+    // The following line we only cover case 2. We can not revise it since it may break some
+    // existing app, but we should do it someday.
+    return (encoded == null || encoded instanceof JSONArray) ? (JSONArray) encoded : defaultValue;
+  }
+
+
+  /**
+   * Access a {@link JSONObject} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it is not a {@link JSONObject}.
+   */
+  public JSONObject getJSONObject(String key) {
+    return getJSONObject(key, null);
+  }
+
+  /**
+   * Access a {@link JSONObject} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link JSONObject}.
+   */
+  public JSONObject getJSONObject(String key, JSONObject defaultValue) {
+    Map map = getMap(key);
+    Object encoded = (map != null) ? PointerEncoder.get().encode(map) : null;
+    //TODO(mengyan) There are actually two cases, getList(key) will return null
+    // case 1: key not exist, in this situation, we should return JSONArray defaultValue
+    // case 2: key exist but value is Json.NULL, in this situation, we should return null
+    // The following line we only cover case 2. We can not revise it since it may break some
+    // existing app, but we should do it someday.
+    return (encoded == null || encoded instanceof JSONObject) ? (JSONObject) encoded : defaultValue;
+  }
+
+  /**
+   * Access a {@link List} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it cannot be converted to a
+   *          {@link List}.
+   */
+  public  List getList(String key) {
+    return getList(key, null);
+  }
+
+  /**
+   * Access a {@link List} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it cannot be
+   *          converted to a {@link List}.
+   */
+  public  List getList(String key, List defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    @SuppressWarnings("unchecked")
+    List returnValue = (value instanceof List) ? (List) value : defaultValue;
+    return returnValue;
+  }
+
+  /**
+   * Access a {@code long} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns 0 if there is no such key or if it is not a number.
+   */
+  public long getLong(String key) {
+    return getLong(key, 0);
+  }
+
+  /**
+   * Access a {@code long} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a number.
+   */
+  public long getLong(String key, long defaultValue) {
+    Number number = getNumber(key);
+    return number != null ? number.longValue() : defaultValue;
+  }
+
+  /**
+   * Access a {@link Map} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return {@code null} if there is no such key or if it cannot be converted to a
+   *          {@link Map}.
+   */
+  public  Map getMap(String key) {
+    return getMap(key, null);
+  }
+
+  /**
+   * Access a {@link Map} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it cannot be converted
+   *         to a {@link Map}.
+   */
+  public  Map getMap(String key, Map defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    @SuppressWarnings("unchecked")
+    Map returnValue = (value instanceof Map) ? (Map) value : defaultValue;
+    return returnValue;
+  }
+
+  /**
+   * Access a numerical value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it is not a {@link Number}.
+   */
+  public Number getNumber(String key) {
+    return getNumber(key, null);
+  }
+
+  /**
+   * Access a numerical value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link Number}.
+   */
+  public Number getNumber(String key, Number defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    return (value instanceof Number) ? (Number) value : defaultValue;
+  }
+
+  /**
+   * Access a {@link ParseFile} value. This function will not perform a network request. Unless the
+   * {@link ParseFile} has been downloaded (e.g. by calling {@link ParseFile#getData()}),
+   * {@link ParseFile#isDataAvailable()} will return false.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return {@code null} if there is no such key or if it is not a {@link ParseFile}.
+   */
+  public ParseFile getParseFile(String key) {
+    return getParseFile(key, null);
+  }
+
+  /**
+   * Access a {@link ParseFile} value, returning a default value if it doesn't exist. This function
+   * will not perform a network request. Unless the {@link ParseFile} has been downloaded
+   * (e.g. by calling {@link ParseFile#getData()}), {@link ParseFile#isDataAvailable()} will return
+   * false.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link ParseFile}.
+   */
+  public ParseFile getParseFile(String key, ParseFile defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    return (value instanceof ParseFile) ? (ParseFile) value : defaultValue;
+  }
+
+  /**
+   * Access a {@link ParseGeoPoint} value.
+   *
+   * @param key
+   *          The key to access the value for
+   * @return {@code null} if there is no such key or if it is not a {@link ParseGeoPoint}.
+   */
+  public ParseGeoPoint getParseGeoPoint(String key) {
+    return getParseGeoPoint(key, null);
+  }
+
+  /**
+   * Access a {@link ParseGeoPoint} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link ParseGeoPoint}.
+   */
+  public ParseGeoPoint getParseGeoPoint(String key, ParseGeoPoint defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    return (value instanceof ParseGeoPoint) ? (ParseGeoPoint) value : defaultValue;
+  }
+
+  /**
+   * Access a {@link String} value.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @return Returns {@code null} if there is no such key or if it is not a {@link String}.
+   */
+  public String getString(String key) {
+    return getString(key, null);
+  }
+
+  /**
+   * Access a {@link String} value, returning a default value if it doesn't exist.
+   *
+   * @param key
+   *          The key to access the value for.
+   * @param defaultValue
+   *          The value to return if the key is not present or has the wrong type.
+   * @return The default value if there is no such key or if it is not a {@link String}.
+   */
+  public String getString(String key, String defaultValue) {
+    if (!params.containsKey(key)) {
+      return defaultValue;
+    }
+    Object value = params.get(key);
+    if (value == null || value == JSONObject.NULL) {
+      return null;
+    }
+    return (value instanceof String) ? (String) value : defaultValue;
+  }
+
+  @Override
+  public String toString() {
+    return "ParseConfig[" + params.toString() + "]";
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfigController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfigController.java
new file mode 100644
index 0000000..d52f7b1
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseConfigController.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import org.json.JSONObject;
+
+import bolts.Continuation;
+import bolts.Task;
+
+/** package */ class ParseConfigController {
+
+  private ParseCurrentConfigController currentConfigController;
+  private final ParseHttpClient restClient;
+
+  public ParseConfigController(ParseHttpClient restClient,
+      ParseCurrentConfigController currentConfigController) {
+    this.restClient = restClient;
+    this.currentConfigController = currentConfigController;
+  }
+  /* package */ ParseCurrentConfigController getCurrentConfigController() {
+    return currentConfigController;
+  }
+
+  public Task getAsync(String sessionToken) {
+    final ParseRESTCommand command = ParseRESTConfigCommand.fetchConfigCommand(sessionToken);
+    return command.executeAsync(restClient).onSuccessTask(new Continuation>() {
+      @Override
+      public Task then(Task task) throws Exception {
+        JSONObject result = task.getResult();
+
+        final ParseConfig config = ParseConfig.decode(result, ParseDecoder.get());
+        return currentConfigController.setCurrentConfigAsync(config).continueWith(new Continuation() {
+          @Override
+          public ParseConfig then(Task task) throws Exception {
+            return config;
+          }
+        });
+      }
+    });
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCorePlugins.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCorePlugins.java
new file mode 100644
index 0000000..bad4e7c
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCorePlugins.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** package */ class ParseCorePlugins {
+
+  private static final ParseCorePlugins INSTANCE = new ParseCorePlugins();
+  public static ParseCorePlugins getInstance() {
+    return INSTANCE;
+  }
+
+  /* package */ static final String FILENAME_CURRENT_USER = "currentUser";
+  /* package */ static final String PIN_CURRENT_USER = "_currentUser";
+  /* package */ static final String FILENAME_CURRENT_INSTALLATION = "currentInstallation";
+  /* package */ static final String PIN_CURRENT_INSTALLATION = "_currentInstallation";
+  /* package */ static final String FILENAME_CURRENT_CONFIG = "currentConfig";
+
+  private AtomicReference objectController = new AtomicReference<>();
+  private AtomicReference userController = new AtomicReference<>();
+  private AtomicReference sessionController = new AtomicReference<>();
+
+  // TODO(mengyan): Inject into ParseUserInstanceController
+  private AtomicReference currentUserController =
+    new AtomicReference<>();
+  // TODO(mengyan): Inject into ParseInstallationInstanceController
+  private AtomicReference currentInstallationController =
+      new AtomicReference<>();
+
+  private AtomicReference authenticationController =
+      new AtomicReference<>();
+
+  private AtomicReference queryController = new AtomicReference<>();
+  private AtomicReference fileController = new AtomicReference<>();
+  private AtomicReference analyticsController = new AtomicReference<>();
+  private AtomicReference cloudCodeController = new AtomicReference<>();
+  private AtomicReference configController = new AtomicReference<>();
+  private AtomicReference pushController = new AtomicReference<>();
+  private AtomicReference pushChannelsController =
+      new AtomicReference<>();
+  private AtomicReference defaultACLController = new AtomicReference<>();
+
+  private AtomicReference localIdManager = new AtomicReference<>();
+  private AtomicReference subclassingController = new AtomicReference<>();
+
+  private ParseCorePlugins() {
+    // do nothing
+  }
+
+  /* package for tests */ void reset() {
+    objectController.set(null);
+    userController.set(null);
+    sessionController.set(null);
+
+    currentUserController.set(null);
+    currentInstallationController.set(null);
+
+    authenticationController.set(null);
+
+    queryController.set(null);
+    fileController.set(null);
+    analyticsController.set(null);
+    cloudCodeController.set(null);
+    configController.set(null);
+    pushController.set(null);
+    pushChannelsController.set(null);
+    defaultACLController.set(null);
+
+    localIdManager.set(null);
+  }
+
+  public ParseObjectController getObjectController() {
+    if (objectController.get() == null) {
+      // TODO(grantland): Do not rely on Parse global
+      objectController.compareAndSet(
+          null, new NetworkObjectController(ParsePlugins.get().restClient()));
+    }
+    return objectController.get();
+  }
+
+  public void registerObjectController(ParseObjectController controller) {
+    if (!objectController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another object controller was already registered: " + objectController.get());
+    }
+  }
+
+  public ParseUserController getUserController() {
+    if (userController.get() == null) {
+      // TODO(grantland): Do not rely on Parse global
+      userController.compareAndSet(
+          null, new NetworkUserController(ParsePlugins.get().restClient()));
+    }
+    return userController.get();
+  }
+
+  public void registerUserController(ParseUserController controller) {
+    if (!userController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another user controller was already registered: " + userController.get());
+    }
+  }
+
+  public ParseSessionController getSessionController() {
+    if (sessionController.get() == null) {
+      // TODO(grantland): Do not rely on Parse global
+      sessionController.compareAndSet(
+          null, new NetworkSessionController(ParsePlugins.get().restClient()));
+    }
+    return sessionController.get();
+  }
+
+  public void registerSessionController(ParseSessionController controller) {
+    if (!sessionController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another session controller was already registered: " + sessionController.get());
+    }
+  }
+
+  public ParseCurrentUserController getCurrentUserController() {
+    if (currentUserController.get() == null) {
+      File file = new File(Parse.getParseDir(), FILENAME_CURRENT_USER);
+      FileObjectStore fileStore =
+          new FileObjectStore<>(ParseUser.class, file, ParseUserCurrentCoder.get());
+      ParseObjectStore store = Parse.isLocalDatastoreEnabled()
+          ? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore)
+          : fileStore;
+      ParseCurrentUserController controller = new CachedCurrentUserController(store);
+      currentUserController.compareAndSet(null, controller);
+    }
+    return currentUserController.get();
+  }
+
+  public void registerCurrentUserController(ParseCurrentUserController controller) {
+    if (!currentUserController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another currentUser controller was already registered: " +
+              currentUserController.get());
+    }
+  }
+
+  public ParseQueryController getQueryController() {
+    if (queryController.get() == null) {
+      NetworkQueryController networkController = new NetworkQueryController(
+          ParsePlugins.get().restClient());
+      ParseQueryController controller;
+      // TODO(grantland): Do not rely on Parse global
+      if (Parse.isLocalDatastoreEnabled()) {
+        controller = new OfflineQueryController(
+            Parse.getLocalDatastore(),
+            networkController);
+      } else {
+        controller = new CacheQueryController(networkController);
+      }
+      queryController.compareAndSet(null, controller);
+    }
+    return queryController.get();
+  }
+
+  public void registerQueryController(ParseQueryController controller) {
+    if (!queryController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another query controller was already registered: " + queryController.get());
+    }
+  }
+
+  public ParseFileController getFileController() {
+    if (fileController.get() == null) {
+      // TODO(grantland): Do not rely on Parse global
+      fileController.compareAndSet(null, new ParseFileController(
+          ParsePlugins.get().restClient(),
+          Parse.getParseCacheDir("files")));
+    }
+    return fileController.get();
+  }
+
+  public void registerFileController(ParseFileController controller) {
+    if (!fileController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another file controller was already registered: " + fileController.get());
+    }
+  }
+
+  public ParseAnalyticsController getAnalyticsController() {
+    if (analyticsController.get() == null) {
+      // TODO(mengyan): Do not rely on Parse global
+      analyticsController.compareAndSet(null,
+          new ParseAnalyticsController(Parse.getEventuallyQueue()));
+    }
+    return analyticsController.get();
+  }
+
+  public void registerAnalyticsController(ParseAnalyticsController controller) {
+    if (!analyticsController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another analytics controller was already registered: " + analyticsController.get());
+    }
+  }
+
+  public ParseCloudCodeController getCloudCodeController() {
+    if (cloudCodeController.get() == null) {
+      cloudCodeController.compareAndSet(null, new ParseCloudCodeController(
+          ParsePlugins.get().restClient()));
+    }
+    return cloudCodeController.get();
+  }
+
+  public void registerCloudCodeController(ParseCloudCodeController controller) {
+    if (!cloudCodeController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another cloud code controller was already registered: " + cloudCodeController.get());
+    }
+  }
+
+  public ParseConfigController getConfigController() {
+    if (configController.get() == null) {
+      // TODO(mengyan): Do not rely on Parse global
+      File file = new File(ParsePlugins.get().getParseDir(), FILENAME_CURRENT_CONFIG);
+      ParseCurrentConfigController currentConfigController =
+          new ParseCurrentConfigController(file);
+      configController.compareAndSet(null, new ParseConfigController(
+          ParsePlugins.get().restClient(), currentConfigController));
+    }
+    return configController.get();
+  }
+
+  public void registerConfigController(ParseConfigController controller) {
+    if (!configController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another config controller was already registered: " + configController.get());
+    }
+  }
+
+  public ParsePushController getPushController() {
+    if (pushController.get() == null) {
+      pushController.compareAndSet(null, new ParsePushController(ParsePlugins.get().restClient()));
+    }
+    return pushController.get();
+  }
+
+  public void registerPushController(ParsePushController controller) {
+    if (!pushController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another push controller was already registered: " + pushController.get());
+    }
+  }
+
+  public ParsePushChannelsController getPushChannelsController() {
+    if (pushChannelsController.get() == null) {
+      pushChannelsController.compareAndSet(null, new ParsePushChannelsController());
+    }
+    return pushChannelsController.get();
+  }
+
+  public void registerPushChannelsController(ParsePushChannelsController controller) {
+    if (!pushChannelsController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another pushChannels controller was already registered: " +
+              pushChannelsController.get());
+    }
+  }
+
+  public ParseCurrentInstallationController getCurrentInstallationController() {
+    if (currentInstallationController.get() == null) {
+      File file = new File(ParsePlugins.get().getParseDir(), FILENAME_CURRENT_INSTALLATION);
+      FileObjectStore fileStore =
+          new FileObjectStore<>(ParseInstallation.class, file, ParseObjectCurrentCoder.get());
+      ParseObjectStore store = Parse.isLocalDatastoreEnabled()
+          ? new OfflineObjectStore<>(ParseInstallation.class, PIN_CURRENT_INSTALLATION, fileStore)
+          : fileStore;
+      CachedCurrentInstallationController controller =
+          new CachedCurrentInstallationController(store, ParsePlugins.get().installationId());
+      currentInstallationController.compareAndSet(null, controller);
+    }
+    return currentInstallationController.get();
+  }
+
+  public void registerCurrentInstallationController(ParseCurrentInstallationController controller) {
+    if (!currentInstallationController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another currentInstallation controller was already registered: " +
+              currentInstallationController.get());
+    }
+  }
+
+  public ParseAuthenticationManager getAuthenticationManager() {
+    if (authenticationController.get() == null) {
+      ParseAuthenticationManager controller =
+          new ParseAuthenticationManager(getCurrentUserController());
+      authenticationController.compareAndSet(null, controller);
+    }
+    return authenticationController.get();
+  }
+
+  public void registerAuthenticationManager(ParseAuthenticationManager manager) {
+    if (!authenticationController.compareAndSet(null, manager)) {
+      throw new IllegalStateException(
+          "Another authentication manager was already registered: " +
+              authenticationController.get());
+    }
+  }
+
+  public ParseDefaultACLController getDefaultACLController() {
+    if (defaultACLController.get() == null) {
+      ParseDefaultACLController controller = new ParseDefaultACLController();
+      defaultACLController.compareAndSet(null, controller);
+    }
+    return defaultACLController.get();
+  }
+
+  public void registerDefaultACLController(ParseDefaultACLController controller) {
+    if (!defaultACLController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another defaultACL controller was already registered: " + defaultACLController.get());
+    }
+  }
+
+  public LocalIdManager getLocalIdManager() {
+    if (localIdManager.get() == null) {
+      LocalIdManager manager = new LocalIdManager(Parse.getParseDir());
+      localIdManager.compareAndSet(null, manager);
+    }
+    return localIdManager.get();
+  }
+
+  public void registerLocalIdManager(LocalIdManager manager) {
+    if (!localIdManager.compareAndSet(null, manager)) {
+      throw new IllegalStateException(
+          "Another localId manager was already registered: " + localIdManager.get());
+    }
+  }
+
+  public ParseObjectSubclassingController getSubclassingController() {
+    if (subclassingController.get() == null) {
+      ParseObjectSubclassingController controller = new ParseObjectSubclassingController();
+      subclassingController.compareAndSet(null, controller);
+    }
+    return subclassingController.get();
+  }
+
+  public void registerSubclassingController(ParseObjectSubclassingController controller) {
+    if (!subclassingController.compareAndSet(null, controller)) {
+      throw new IllegalStateException(
+          "Another subclassing controller was already registered: " + subclassingController.get());
+    }
+  }
+}
+
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingByteArrayHttpBody.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingByteArrayHttpBody.java
new file mode 100644
index 0000000..cc907b4
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingByteArrayHttpBody.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+
+import bolts.Task;
+
+import static java.lang.Math.min;
+
+/** package */ class ParseCountingByteArrayHttpBody extends ParseByteArrayHttpBody {
+  private static final int DEFAULT_CHUNK_SIZE = 4096;
+  private final ProgressCallback progressCallback;
+
+  public ParseCountingByteArrayHttpBody(byte[] content, String contentType,
+      final ProgressCallback progressCallback) {
+    super(content, contentType);
+    this.progressCallback = progressCallback;
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException {
+    if (out == null) {
+      throw new IllegalArgumentException("Output stream may not be null");
+    }
+
+    int position = 0;
+    int totalLength = content.length;
+    while (position < totalLength) {
+      int length = min(totalLength - position, DEFAULT_CHUNK_SIZE);
+
+      out.write(content, position, length);
+      out.flush();
+
+      if (progressCallback != null) {
+        position += length;
+
+        int progress = 100 * position / totalLength;
+        progressCallback.done(progress);
+      }
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingFileHttpBody.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingFileHttpBody.java
new file mode 100644
index 0000000..3ae984d
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCountingFileHttpBody.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** package */ class ParseCountingFileHttpBody extends ParseFileHttpBody {
+
+  private static final int DEFAULT_CHUNK_SIZE = 4096;
+  private static final int EOF = -1;
+
+  private final ProgressCallback progressCallback;
+
+  public ParseCountingFileHttpBody(File file, ProgressCallback progressCallback) {
+    this(file, null, progressCallback);
+  }
+
+  public ParseCountingFileHttpBody(
+      File file, String contentType, ProgressCallback progressCallback) {
+    super(file, contentType);
+    this.progressCallback = progressCallback;
+  }
+
+  @Override
+  public void writeTo(OutputStream output) throws IOException {
+    if (output == null) {
+      throw new IllegalArgumentException("Output stream may not be null");
+    }
+
+    final FileInputStream fileInput = new FileInputStream(file);
+    try {
+      byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];
+      int n;
+      long totalLength = file.length();
+      long position = 0;
+      while (EOF != (n = fileInput.read(buffer))) {
+        output.write(buffer, 0, n);
+        position += n;
+
+        if (progressCallback != null) {
+          int progress = (int) (100 * position / totalLength);
+          progressCallback.done(progress);
+        }
+      }
+    } finally {
+      ParseIOUtils.closeQuietly(fileInput);
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentConfigController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentConfigController.java
new file mode 100644
index 0000000..f3e7b66
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentConfigController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import bolts.Task;
+
+/** package */ class ParseCurrentConfigController {
+
+  private final Object currentConfigMutex = new Object();
+  /* package for test */ ParseConfig currentConfig;
+  private File currentConfigFile;
+
+  public ParseCurrentConfigController(File currentConfigFile) {
+    this.currentConfigFile = currentConfigFile;
+  }
+
+  public Task setCurrentConfigAsync(final ParseConfig config) {
+    return Task.call(new Callable() {
+      @Override
+      public Void call() throws Exception {
+        synchronized (currentConfigMutex) {
+          currentConfig = config;
+          saveToDisk(config);
+        }
+        return null;
+      }
+    }, ParseExecutors.io());
+  }
+
+  public Task getCurrentConfigAsync() {
+    return Task.call(new Callable() {
+      @Override
+      public ParseConfig call() throws Exception {
+        synchronized (currentConfigMutex) {
+          if (currentConfig == null) {
+            ParseConfig config = getFromDisk();
+            currentConfig = (config != null) ? config : new ParseConfig();
+          }
+        }
+        return currentConfig;
+      }
+    }, ParseExecutors.io());
+  }
+
+  /**
+   * Retrieves a {@code ParseConfig} from a file on disk.
+   *
+   * @return The {@code ParseConfig} that was retrieved. If the file wasn't found, or the contents
+   *          of the file is an invalid {@code ParseConfig}, returns null.
+   */
+  /* package for test */ ParseConfig getFromDisk() {
+    JSONObject json;
+    try {
+      json = ParseFileUtils.readFileToJSONObject(currentConfigFile);
+    } catch (IOException | JSONException e) {
+      return null;
+    }
+    return ParseConfig.decode(json, ParseDecoder.get());
+  }
+
+  /* package */ void clearCurrentConfigForTesting() {
+    synchronized (currentConfigMutex) {
+      currentConfig = null;
+    }
+  }
+
+  /**
+   * Saves the {@code ParseConfig} to the a file on disk as JSON.
+   *
+   * @param config
+   *          The ParseConfig which needs to be saved.
+   */
+  /* package for test */ void saveToDisk(ParseConfig config) {
+    JSONObject object = new JSONObject();
+    try {
+      JSONObject jsonParams = (JSONObject) NoObjectsEncoder.get().encode(config.getParams());
+      object.put("params", jsonParams);
+    } catch (JSONException e) {
+      throw new RuntimeException("could not serialize config to JSON");
+    }
+    try {
+      ParseFileUtils.writeJSONObjectToFile(currentConfigFile, object);
+    } catch (IOException e) {
+      //TODO (grantland): We should do something if this fails...
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentInstallationController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentInstallationController.java
new file mode 100644
index 0000000..2a8d165
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentInstallationController.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+/** package */ interface ParseCurrentInstallationController
+    extends ParseObjectCurrentController {
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentUserController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentUserController.java
new file mode 100644
index 0000000..0805ae2
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseCurrentUserController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import bolts.Task;
+
+/** package */ interface ParseCurrentUserController
+    extends ParseObjectCurrentController {
+
+  /**
+   * Gets the persisted current ParseUser.
+   * @param shouldAutoCreateUser
+   * @return
+   */
+  Task getAsync(boolean shouldAutoCreateUser);
+
+  /**
+   * Sets the persisted current ParseUser only if it's current or we're not synced with disk.
+   * @param user
+   * @return
+   */
+  Task setIfNeededAsync(ParseUser user);
+
+  /**
+   * Gets the session token of the persisted current ParseUser.
+   * @return
+   */
+  Task getCurrentSessionTokenAsync();
+
+  /**
+   * Logs out the current ParseUser.
+   * @return
+   */
+  Task logOutAsync();
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDateFormat.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDateFormat.java
new file mode 100644
index 0000000..20c9998
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDateFormat.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+
+/**
+ * This is the currently used date format. It is precise to the millisecond.
+ */
+/* package */ class ParseDateFormat {
+  private static final String TAG = "ParseDateFormat";
+
+  private static final ParseDateFormat INSTANCE = new ParseDateFormat();
+  public static ParseDateFormat getInstance() {
+    return INSTANCE;
+  }
+
+  // SimpleDateFormat isn't inherently thread-safe
+  private final Object lock = new Object();
+
+  private final DateFormat dateFormat;
+
+  private ParseDateFormat() {
+    DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+    format.setTimeZone(new SimpleTimeZone(0, "GMT"));
+    dateFormat = format;
+  }
+
+  /* package */ Date parse(String dateString) {
+    synchronized (lock) {
+      try {
+        return dateFormat.parse(dateString);
+      } catch (java.text.ParseException e) {
+        // Should never happen
+        PLog.e(TAG, "could not parse date: " + dateString, e);
+        return null;
+      }
+    }
+  }
+
+  /* package */ String format(Date date) {
+    synchronized (lock) {
+      return dateFormat.format(date);
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDecoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDecoder.java
new file mode 100644
index 0000000..a8f961f
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDecoder.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.util.Base64;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A {@code ParseDecoder} can be used to transform JSON data structures into actual objects, such as
+ * {@link ParseObjects}.
+ *
+ * @see com.parse.ParseEncoder
+ */
+/** package */ class ParseDecoder {
+
+  // This class isn't really a Singleton, but since it has no state, it's more efficient to get the
+  // default instance.
+  private static final ParseDecoder INSTANCE = new ParseDecoder();
+  public static ParseDecoder get() {
+    return INSTANCE;
+  }
+
+  protected ParseDecoder() {
+    // do nothing
+  }
+
+  /* package */ List convertJSONArrayToList(JSONArray array) {
+    List list = new ArrayList<>();
+    for (int i = 0; i < array.length(); ++i) {
+      list.add(decode(array.opt(i)));
+    }
+    return list;
+  }
+
+  /* package */ Map convertJSONObjectToMap(JSONObject object) {
+    Map outputMap = new HashMap<>();
+    Iterator it = object.keys();
+    while (it.hasNext()) {
+      String key = it.next();
+      Object value = object.opt(key);
+      outputMap.put(key, decode(value));
+    }
+    return outputMap;
+  }
+
+  /**
+   * Gets the ParseObject another object points to. By default a new
+   * object will be created.
+   */
+  protected ParseObject decodePointer(String className, String objectId) {
+    return ParseObject.createWithoutData(className, objectId);
+  }
+
+  public Object decode(Object object) {
+    if (object instanceof JSONArray) {
+      return convertJSONArrayToList((JSONArray) object);
+    }
+
+    if (object == JSONObject.NULL) {
+      return null;
+    }
+
+    if (!(object instanceof JSONObject)) {
+      return object;
+    }
+
+    JSONObject jsonObject = (JSONObject) object;
+
+    String opString = jsonObject.optString("__op", null);
+    if (opString != null) {
+      try {
+        return ParseFieldOperations.decode(jsonObject, this);
+      } catch (JSONException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    String typeString = jsonObject.optString("__type", null);
+    if (typeString == null) {
+      return convertJSONObjectToMap(jsonObject);
+    }
+
+    if (typeString.equals("Date")) {
+      String iso = jsonObject.optString("iso");
+      return ParseDateFormat.getInstance().parse(iso);
+    }
+
+    if (typeString.equals("Bytes")) {
+      String base64 = jsonObject.optString("base64");
+      return Base64.decode(base64, Base64.NO_WRAP);
+    }
+
+    if (typeString.equals("Pointer")) {
+      return decodePointer(jsonObject.optString("className"),
+          jsonObject.optString("objectId"));
+    }
+
+    if (typeString.equals("File")) {
+      return new ParseFile(jsonObject, this);
+    }
+
+    if (typeString.equals("GeoPoint")) {
+      double latitude, longitude;
+      try {
+        latitude = jsonObject.getDouble("latitude");
+        longitude = jsonObject.getDouble("longitude");
+      } catch (JSONException e) {
+        throw new RuntimeException(e);
+      }
+      return new ParseGeoPoint(latitude, longitude);
+    }
+
+    if (typeString.equals("Polygon")) {
+      List coordinates = new ArrayList();
+      try {
+        JSONArray array = jsonObject.getJSONArray("coordinates");
+        for (int i = 0; i < array.length(); ++i) {
+          JSONArray point = array.getJSONArray(i);
+          coordinates.add(new ParseGeoPoint(point.getDouble(0), point.getDouble(1)));
+        }
+      } catch (JSONException e) {
+        throw new RuntimeException(e);
+      }
+      return new ParsePolygon(coordinates);
+    }
+
+    if (typeString.equals("Object")) {
+      return ParseObject.fromJSON(jsonObject, null, this);
+    }
+
+    if (typeString.equals("Relation")) {
+      return new ParseRelation<>(jsonObject, this);
+    }
+
+    if (typeString.equals("OfflineObject")) {
+      throw new RuntimeException("An unexpected offline pointer was encountered.");
+    }
+
+    return null;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDefaultACLController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDefaultACLController.java
new file mode 100644
index 0000000..c2e9329
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDefaultACLController.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.lang.ref.WeakReference;
+
+/** package */ class ParseDefaultACLController {
+
+  /* package for tests */ ParseACL defaultACL;
+  /* package for tests */ boolean defaultACLUsesCurrentUser;
+  /* package for tests */ WeakReference lastCurrentUser;
+  /* package for tests */ ParseACL defaultACLWithCurrentUser;
+
+  /**
+   * Sets a default ACL that will be applied to all {@link ParseObject}s when they are created.
+   *
+   * @param acl
+   *          The ACL to use as a template for all {@link ParseObject}s created after set
+   *          has been called. This value will be copied and used as a template for the creation of
+   *          new ACLs, so changes to the instance after {@code set(ParseACL, boolean)}
+   *          has been called will not be reflected in new {@link ParseObject}s.
+   * @param withAccessForCurrentUser
+   *          If {@code true}, the {@code ParseACL} that is applied to newly-created
+   *          {@link ParseObject}s will provide read and write access to the
+   *          {@link ParseUser#getCurrentUser()} at the time of creation. If {@code false}, the
+   *          provided ACL will be used without modification. If acl is {@code null}, this value is
+   *          ignored.
+   */
+  public void set(ParseACL acl, boolean withAccessForCurrentUser) {
+    defaultACLWithCurrentUser = null;
+    lastCurrentUser = null;
+    if (acl != null) {
+      ParseACL newDefaultACL = acl.copy();
+      newDefaultACL.setShared(true);
+      defaultACL = newDefaultACL;
+      defaultACLUsesCurrentUser = withAccessForCurrentUser;
+    } else {
+      defaultACL = null;
+    }
+  }
+
+  public ParseACL get() {
+    if (defaultACLUsesCurrentUser && defaultACL != null) {
+      ParseUser currentUser = ParseUser.getCurrentUser();
+      if (currentUser != null) {
+        // If the currentUser has changed, generate a new ACL from the defaultACL.
+        ParseUser last = lastCurrentUser != null ? lastCurrentUser.get() : null;
+        if (last != currentUser) {
+          ParseACL newDefaultACLWithCurrentUser = defaultACL.copy();
+          newDefaultACLWithCurrentUser.setShared(true);
+          newDefaultACLWithCurrentUser.setReadAccess(currentUser, true);
+          newDefaultACLWithCurrentUser.setWriteAccess(currentUser, true);
+          defaultACLWithCurrentUser = newDefaultACLWithCurrentUser;
+          lastCurrentUser = new WeakReference<>(currentUser);
+        }
+        return defaultACLWithCurrentUser;
+      }
+    }
+    return defaultACL;
+  }
+
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDeleteOperation.java
new file mode 100644
index 0000000..fb5393a
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDeleteOperation.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.os.Parcel;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * An operation where a field is deleted from the object.
+ */
+/** package */ class ParseDeleteOperation implements ParseFieldOperation {
+  /* package */ final static String OP_NAME = "Delete";
+
+  private static final ParseDeleteOperation defaultInstance = new ParseDeleteOperation();
+
+  public static ParseDeleteOperation getInstance() {
+    return defaultInstance;
+  }
+
+  private ParseDeleteOperation() {
+  }
+
+  @Override
+  public JSONObject encode(ParseEncoder objectEncoder) throws JSONException {
+    JSONObject output = new JSONObject();
+    output.put("__op", OP_NAME);
+    return output;
+  }
+
+  @Override
+  public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) {
+    dest.writeString(OP_NAME);
+  }
+
+  @Override
+  public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) {
+    return this;
+  }
+
+  @Override
+  public Object apply(Object oldValue, String key) {
+    return null;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDigestUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDigestUtils.java
new file mode 100644
index 0000000..21998b2
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseDigestUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Static utility helpers to compute {@link MessageDigest}s.
+ */
+/* package */ class ParseDigestUtils {
+
+  private static final char[] hexArray = "0123456789abcdef".toCharArray();
+
+  private ParseDigestUtils() {
+    // no instances allowed
+  }
+
+  public static String md5(String string) {
+    MessageDigest digester;
+    try {
+      digester = MessageDigest.getInstance("MD5");
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException(e);
+    }
+
+    digester.update(string.getBytes());
+    byte[] digest = digester.digest();
+    return toHex(digest);
+  }
+
+  private static String toHex(byte[] bytes) {
+    // The returned string will be double the length of the passed array, as it takes two
+    // characters to represent any given byte.
+    char[] hexChars = new char[bytes.length * 2];
+    for ( int j = 0; j < bytes.length; j++ ) {
+      int v = bytes[j] & 0xFF;
+      hexChars[j * 2] = hexArray[v >>> 4];
+      hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+    }
+    return new String(hexChars);
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEncoder.java
new file mode 100644
index 0000000..05588de
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEncoder.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.util.Base64;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@code ParseEncoder} can be used to transform objects such as {@link ParseObjects} into JSON
+ * data structures.
+ *
+ * @see com.parse.ParseDecoder
+ */
+/** package */ abstract class ParseEncoder {
+
+  /* package */ static boolean isValidType(Object value) {
+    return value instanceof String
+        || value instanceof Number
+        || value instanceof Boolean
+        || value instanceof Date
+        || value instanceof List
+        || value instanceof Map
+        || value instanceof byte[]
+        || value == JSONObject.NULL
+        || value instanceof ParseObject
+        || value instanceof ParseACL
+        || value instanceof ParseFile
+        || value instanceof ParseGeoPoint
+        || value instanceof ParsePolygon
+        || value instanceof ParseRelation;
+  }
+
+  public Object encode(Object object) {
+    try {
+      if (object instanceof ParseObject) {
+        return encodeRelatedObject((ParseObject) object);
+      }
+
+      // TODO(grantland): Remove once we disallow mutable nested queries t6941155
+      if (object instanceof ParseQuery.State.Builder) {
+        ParseQuery.State.Builder builder = (ParseQuery.State.Builder) object;
+        return encode(builder.build());
+      }
+
+      if (object instanceof ParseQuery.State) {
+        ParseQuery.State state = (ParseQuery.State) object;
+        return state.toJSON(this);
+      }
+
+      if (object instanceof Date) {
+        return encodeDate((Date) object);
+      }
+
+      if (object instanceof byte[]) {
+        JSONObject json = new JSONObject();
+        json.put("__type", "Bytes");
+        json.put("base64", Base64.encodeToString((byte[]) object, Base64.NO_WRAP));
+        return json;
+      }
+
+      if (object instanceof ParseFile) {
+        return ((ParseFile) object).encode();
+      }
+
+      if (object instanceof ParseGeoPoint) {
+        ParseGeoPoint point = (ParseGeoPoint) object;
+        JSONObject json = new JSONObject();
+        json.put("__type", "GeoPoint");
+        json.put("latitude", point.getLatitude());
+        json.put("longitude", point.getLongitude());
+        return json;
+      }
+
+      if (object instanceof ParsePolygon) {
+        ParsePolygon polygon = (ParsePolygon) object;
+        JSONObject json = new JSONObject();
+        json.put("__type", "Polygon");
+        json.put("coordinates", polygon.coordinatesToJSONArray());
+        return json;
+      }
+
+      if (object instanceof ParseACL) {
+        ParseACL acl = (ParseACL) object;
+        return acl.toJSONObject(this);
+      }
+
+      if (object instanceof Map) {
+        @SuppressWarnings("unchecked")
+        Map map = (Map) object;
+        JSONObject json = new JSONObject();
+        for (Map.Entry pair : map.entrySet()) {
+          json.put(pair.getKey(), encode(pair.getValue()));
+        }
+        return json;
+      }
+
+      if (object instanceof Collection) {
+        JSONArray array = new JSONArray();
+        for (Object item : (Collection) object) {
+          array.put(encode(item));
+        }
+        return array;
+      }
+
+      if (object instanceof ParseRelation) {
+        ParseRelation relation = (ParseRelation) object;
+        return relation.encodeToJSON(this);
+      }
+
+      if (object instanceof ParseFieldOperation) {
+        return ((ParseFieldOperation) object).encode(this);
+      }
+
+      if (object instanceof ParseQuery.RelationConstraint) {
+        return ((ParseQuery.RelationConstraint) object).encode(this);
+      }
+
+      if (object == null) {
+        return JSONObject.NULL;
+      }
+
+    } catch (JSONException e) {
+      throw new RuntimeException(e);
+    }
+
+    // String, Number, Boolean,
+    if (isValidType(object)) {
+      return object;
+    }
+
+    throw new IllegalArgumentException("invalid type for ParseObject: "
+        + object.getClass().toString());
+  }
+
+  protected abstract JSONObject encodeRelatedObject(ParseObject object);
+
+  protected JSONObject encodeDate(Date date) {
+    JSONObject object = new JSONObject();
+    String iso = ParseDateFormat.getInstance().format(date);
+    try {
+      object.put("__type", "Date");
+      object.put("iso", iso);
+    } catch (JSONException e) {
+      // This should not happen
+      throw new RuntimeException(e);
+    }
+    return object;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEventuallyQueue.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEventuallyQueue.java
new file mode 100644
index 0000000..82dd238
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseEventuallyQueue.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.util.SparseArray;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import bolts.Task;
+
+/* package */ abstract class ParseEventuallyQueue {
+
+  private boolean isConnected;
+
+  /**
+   * Gets notifications of various events happening in the command cache, so that tests can be
+   * synchronized.
+   */
+  private TestHelper testHelper;
+
+  public abstract void onDestroy();
+
+  public void setConnected(boolean connected) {
+    isConnected = connected;
+  }
+
+  public boolean isConnected() {
+    return isConnected;
+  }
+
+  public abstract int pendingCount();
+
+  public void setTimeoutRetryWaitSeconds(double seconds) {
+    // do nothing
+  }
+
+  public void setMaxCacheSizeBytes(int bytes) {
+    // do nothing
+  }
+
+  /** See class TestHelper below. */
+  public TestHelper getTestHelper() {
+    if (testHelper == null) {
+      testHelper = new TestHelper();
+    }
+    return testHelper;
+  }
+
+  protected void notifyTestHelper(int event) {
+    notifyTestHelper(event, null);
+  }
+
+  protected void notifyTestHelper(int event, Throwable t) {
+    if (testHelper != null) {
+      testHelper.notify(event, t);
+    }
+  }
+
+  public abstract void pause();
+
+  public abstract void resume();
+
+  /**
+   * Attempts to run the given command and any pending commands. Adds the command to the pending set
+   * if it can't be run yet.
+   *
+   * @param command
+   *          - The command to run.
+   * @param object
+   *          - If this command references an unsaved object, we need to remove any previous command
+   *          referencing that unsaved object. Otherwise, it will get created over and over again.
+   *          So object is a reference to the object, if it has no objectId. Otherwise, it can be
+   *          null.
+   */
+  public abstract Task enqueueEventuallyAsync(ParseRESTCommand command,
+      ParseObject object);
+
+  protected ParseRESTCommand commandFromJSON(JSONObject json)
+      throws JSONException {
+    ParseRESTCommand command = null;
+    if (ParseRESTCommand.isValidCommandJSONObject(json)) {
+      command = ParseRESTCommand.fromJSONObject(json);
+    } else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) {
+      // do nothing
+    } else {
+      throw new JSONException("Failed to load command from JSON.");
+    }
+    return command;
+  }
+
+  /* package */ Task waitForOperationSetAndEventuallyPin(ParseOperationSet operationSet,
+        EventuallyPin eventuallyPin) {
+    return Task.forResult(null);
+  }
+
+  /* package */ abstract void simulateReboot();
+
+  /**
+   * Gets rid of all pending commands.
+   */
+  public abstract void clear();
+
+  /**
+   * Fakes an object update notification for use in tests. This is used by saveEventually to make it
+   * look like test code has updated an object through the command cache even if it actually
+   * avoided executing update by determining the object wasn't dirty.
+   */
+  void fakeObjectUpdate() {
+    if (testHelper != null) {
+      testHelper.notify(TestHelper.COMMAND_ENQUEUED);
+      testHelper.notify(TestHelper.COMMAND_SUCCESSFUL);
+      testHelper.notify(TestHelper.OBJECT_UPDATED);
+    }
+  }
+
+  /**
+   * Gets notifications of various events happening in the command cache, so that tests can be
+   * synchronized. See ParseCommandCacheTest for examples of how to use this.
+   */
+  public static class TestHelper {
+    private static final int MAX_EVENTS = 1000;
+
+    public static final int COMMAND_SUCCESSFUL = 1;
+    public static final int COMMAND_FAILED = 2;
+    public static final int COMMAND_ENQUEUED = 3;
+    public static final int COMMAND_NOT_ENQUEUED = 4;
+    public static final int OBJECT_UPDATED = 5;
+    public static final int OBJECT_REMOVED = 6;
+    public static final int NETWORK_DOWN = 7;
+    public static final int COMMAND_OLD_FORMAT_DISCARDED = 8;
+
+    public static String getEventString(int event) {
+      switch (event) {
+        case COMMAND_SUCCESSFUL:
+          return "COMMAND_SUCCESSFUL";
+        case COMMAND_FAILED:
+          return "COMMAND_FAILED";
+        case COMMAND_ENQUEUED:
+          return "COMMAND_ENQUEUED";
+        case COMMAND_NOT_ENQUEUED:
+          return "COMMAND_NOT_ENQUEUED";
+        case OBJECT_UPDATED:
+          return "OBJECT_UPDATED";
+        case OBJECT_REMOVED:
+          return "OBJECT_REMOVED";
+        case NETWORK_DOWN:
+          return "NETWORK_DOWN";
+        case COMMAND_OLD_FORMAT_DISCARDED:
+          return "COMMAND_OLD_FORMAT_DISCARDED";
+        default:
+          throw new IllegalStateException("Encountered unknown event: " + event);
+      }
+    }
+
+    private SparseArray events = new SparseArray<>();
+
+    private TestHelper() {
+      clear();
+    }
+
+    public void clear() {
+      events.clear();
+      events.put(COMMAND_SUCCESSFUL, new Semaphore(MAX_EVENTS));
+      events.put(COMMAND_FAILED, new Semaphore(MAX_EVENTS));
+      events.put(COMMAND_ENQUEUED, new Semaphore(MAX_EVENTS));
+      events.put(COMMAND_NOT_ENQUEUED, new Semaphore(MAX_EVENTS));
+      events.put(OBJECT_UPDATED, new Semaphore(MAX_EVENTS));
+      events.put(OBJECT_REMOVED, new Semaphore(MAX_EVENTS));
+      events.put(NETWORK_DOWN, new Semaphore(MAX_EVENTS));
+      events.put(COMMAND_OLD_FORMAT_DISCARDED, new Semaphore(MAX_EVENTS));
+      for (int i = 0; i < events.size(); i++) {
+        int event = events.keyAt(i);
+        events.get(event).acquireUninterruptibly(MAX_EVENTS);
+      }
+    }
+
+    public int unexpectedEvents() {
+      int sum = 0;
+      for (int i = 0; i < events.size(); i++) {
+        int event = events.keyAt(i);
+        sum += events.get(event).availablePermits();
+      }
+      return sum;
+    }
+
+    public List getUnexpectedEvents() {
+      List unexpectedEvents = new ArrayList<>();
+      for (int i = 0; i < events.size(); i++) {
+        int event = events.keyAt(i);
+        if (events.get(event).availablePermits() > 0) {
+          unexpectedEvents.add(getEventString(event));
+        }
+      }
+      return unexpectedEvents;
+    }
+
+    public void notify(int event) {
+      notify(event, null);
+    }
+
+    public void notify(int event, Throwable t) {
+      events.get(event).release();
+    }
+
+    public boolean waitFor(int event) {
+      return waitFor(event, 1);
+    }
+
+    public boolean waitFor(int event, int permits) {
+      try {
+        return events.get(event).tryAcquire(permits, 10, TimeUnit.SECONDS);
+      } catch (InterruptedException e) {
+        e.printStackTrace();
+        return false;
+      }
+    }
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseException.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseException.java
new file mode 100644
index 0000000..49148c7
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseException.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+/**
+ * A ParseException gets raised whenever a {@link ParseObject} issues an invalid request, such as
+ * deleting or editing an object that no longer exists on the server, or when there is a network
+ * failure preventing communication with the Parse server.
+ */
+public class ParseException extends Exception {
+  private static final long serialVersionUID = 1;
+  private int code;
+
+  public static final int OTHER_CAUSE = -1;
+
+  /**
+   * Error code indicating the connection to the Parse servers failed.
+   */
+  public static final int CONNECTION_FAILED = 100;
+
+  /**
+   * Error code indicating the specified object doesn't exist.
+   */
+  public static final int OBJECT_NOT_FOUND = 101;
+
+  /**
+   * Error code indicating you tried to query with a datatype that doesn't support it, like exact
+   * matching an array or object.
+   */
+  public static final int INVALID_QUERY = 102;
+
+  /**
+   * Error code indicating a missing or invalid classname. Classnames are case-sensitive. They must
+   * start with a letter, and a-zA-Z0-9_ are the only valid characters.
+   */
+  public static final int INVALID_CLASS_NAME = 103;
+
+  /**
+   * Error code indicating an unspecified object id.
+   */
+  public static final int MISSING_OBJECT_ID = 104;
+
+  /**
+   * Error code indicating an invalid key name. Keys are case-sensitive. They must start with a
+   * letter, and a-zA-Z0-9_ are the only valid characters.
+   */
+  public static final int INVALID_KEY_NAME = 105;
+
+  /**
+   * Error code indicating a malformed pointer. You should not see this unless you have been mucking
+   * about changing internal Parse code.
+   */
+  public static final int INVALID_POINTER = 106;
+
+  /**
+   * Error code indicating that badly formed JSON was received upstream. This either indicates you
+   * have done something unusual with modifying how things encode to JSON, or the network is failing
+   * badly.
+   */
+  public static final int INVALID_JSON = 107;
+
+  /**
+   * Error code indicating that the feature you tried to access is only available internally for
+   * testing purposes.
+   */
+  public static final int COMMAND_UNAVAILABLE = 108;
+
+  /**
+   * You must call Parse.initialize before using the Parse library.
+   */
+  public static final int NOT_INITIALIZED = 109;
+
+  /**
+   * Error code indicating that a field was set to an inconsistent type.
+   */
+  public static final int INCORRECT_TYPE = 111;
+
+  /**
+   * Error code indicating an invalid channel name. A channel name is either an empty string (the
+   * broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter.
+   */
+  public static final int INVALID_CHANNEL_NAME = 112;
+
+  /**
+   * Error code indicating that push is misconfigured.
+   */
+  public static final int PUSH_MISCONFIGURED = 115;
+
+  /**
+   * Error code indicating that the object is too large.
+   */
+  public static final int OBJECT_TOO_LARGE = 116;
+
+  /**
+   * Error code indicating that the operation isn't allowed for clients.
+   */
+  public static final int OPERATION_FORBIDDEN = 119;
+
+  /**
+   * Error code indicating the result was not found in the cache.
+   */
+  public static final int CACHE_MISS = 120;
+
+  /**
+   * Error code indicating that an invalid key was used in a nested JSONObject.
+   */
+  public static final int INVALID_NESTED_KEY = 121;
+
+  /**
+   * Error code indicating that an invalid filename was used for ParseFile. A valid file name
+   * contains only a-zA-Z0-9_. characters and is between 1 and 128 characters.
+   */
+  public static final int INVALID_FILE_NAME = 122;
+
+  /**
+   * Error code indicating an invalid ACL was provided.
+   */
+  public static final int INVALID_ACL = 123;
+
+  /**
+   * Error code indicating that the request timed out on the server. Typically this indicates that
+   * the request is too expensive to run.
+   */
+  public static final int TIMEOUT = 124;
+
+  /**
+   * Error code indicating that the email address was invalid.
+   */
+  public static final int INVALID_EMAIL_ADDRESS = 125;
+
+  /**
+   * Error code indicating that required field is missing.
+   */
+  public static final int MISSING_REQUIRED_FIELD_ERROR = 135;
+
+  /**
+   * Error code indicating that a unique field was given a value that is already taken.
+   */
+  public static final int DUPLICATE_VALUE = 137;
+
+  /**
+   * Error code indicating that a role's name is invalid.
+   */
+  public static final int INVALID_ROLE_NAME = 139;
+
+  /**
+   * Error code indicating that an application quota was exceeded. Upgrade to resolve.
+   */
+  public static final int EXCEEDED_QUOTA = 140;
+  /**
+   * Error code indicating that a Cloud Code script failed.
+   */
+  public static final int SCRIPT_ERROR = 141;
+  /**
+   * Error code indicating that cloud code validation failed.
+   */
+  public static final int VALIDATION_ERROR = 142;
+
+  /**
+   * Error code indicating that deleting a file failed.
+   */
+  public static final int FILE_DELETE_ERROR = 153;
+
+  /**
+   * Error code indicating that the application has exceeded its request limit.
+   */
+  public static final int REQUEST_LIMIT_EXCEEDED = 155;
+
+  /**
+   * Error code indicating that the provided event name is invalid.
+   */
+  public static final int INVALID_EVENT_NAME = 160;
+
+  /**
+   * Error code indicating that the username is missing or empty.
+   */
+  public static final int USERNAME_MISSING = 200;
+
+  /**
+   * Error code indicating that the password is missing or empty.
+   */
+  public static final int PASSWORD_MISSING = 201;
+
+  /**
+   * Error code indicating that the username has already been taken.
+   */
+  public static final int USERNAME_TAKEN = 202;
+
+  /**
+   * Error code indicating that the email has already been taken.
+   */
+  public static final int EMAIL_TAKEN = 203;
+
+  /**
+   * Error code indicating that the email is missing, but must be specified.
+   */
+  public static final int EMAIL_MISSING = 204;
+
+  /**
+   * Error code indicating that a user with the specified email was not found.
+   */
+  public static final int EMAIL_NOT_FOUND = 205;
+
+  /**
+   * Error code indicating that a user object without a valid session could not be altered.
+   */
+  public static final int SESSION_MISSING = 206;
+
+  /**
+   * Error code indicating that a user can only be created through signup.
+   */
+  public static final int MUST_CREATE_USER_THROUGH_SIGNUP = 207;
+
+  /**
+   * Error code indicating that an an account being linked is already linked to another user.
+   */
+  public static final int ACCOUNT_ALREADY_LINKED = 208;
+
+  /**
+   * Error code indicating that the current session token is invalid.
+   */
+  public static final int INVALID_SESSION_TOKEN = 209;
+
+  /**
+   * Error code indicating that a user cannot be linked to an account because that account's id
+   * could not be found.
+   */
+  public static final int LINKED_ID_MISSING = 250;
+
+  /**
+   * Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session.
+   */
+  public static final int INVALID_LINKED_SESSION = 251;
+
+  /**
+   * Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported.
+   */
+  public static final int UNSUPPORTED_SERVICE = 252;
+
+  /**
+   * Construct a new ParseException with a particular error code.
+   * 
+   * @param theCode
+   *          The error code to identify the type of exception.
+   * @param theMessage
+   *          A message describing the error in more detail.
+   */
+  public ParseException(int theCode, String theMessage) {
+    super(theMessage);
+    code = theCode;
+  }
+
+  /**
+   * Construct a new ParseException with an external cause.
+   * 
+   * @param message
+   *          A message describing the error in more detail.
+   * @param cause
+   *          The cause of the error.
+   */
+  public ParseException(int theCode, String message, Throwable cause) {
+    super(message, cause);
+    code = theCode;
+  }
+
+  /**
+   * Construct a new ParseException with an external cause.
+   * 
+   * @param cause
+   *          The cause of the error.
+   */
+  public ParseException(Throwable cause) {
+    super(cause);
+    code = OTHER_CAUSE;
+  }
+
+  /**
+   * Access the code for this error.
+   * 
+   * @return The numerical code for this error.
+   */
+  public int getCode() {
+    return code;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseExecutors.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseExecutors.java
new file mode 100644
index 0000000..2a9f7df
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseExecutors.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+import bolts.Task;
+
+/** package */ class ParseExecutors {
+
+  private static ScheduledExecutorService scheduledExecutor;
+  private static final Object SCHEDULED_EXECUTOR_LOCK = new Object();
+  /**
+   * Long running operations should NOT be put onto SCHEDULED_EXECUTOR.
+   */
+  /* package */ static ScheduledExecutorService scheduled() {
+    synchronized (SCHEDULED_EXECUTOR_LOCK) {
+      if (scheduledExecutor == null) {
+        scheduledExecutor = java.util.concurrent.Executors.newScheduledThreadPool(1);
+      }
+    }
+    return scheduledExecutor;
+  }
+
+  /* package */ static Executor main() {
+    return Task.UI_THREAD_EXECUTOR;
+  }
+
+  /* package */ static Executor io() {
+    return Task.BACKGROUND_EXECUTOR;
+  }
+}
diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperation.java
new file mode 100644
index 0000000..34abc67
--- /dev/null
+++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperation.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+package com.parse;
+
+import android.os.Parcel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A ParseFieldOperation represents a modification to a value in a ParseObject. For example,
+ * setting, deleting, or incrementing a value are all different kinds of ParseFieldOperations.
+ * ParseFieldOperations themselves can be considered to be immutable.
+ */
+/** package */ interface ParseFieldOperation {
+  /**
+   * Converts the ParseFieldOperation to a data structure (typically a JSONObject) that can be
+   * converted to JSON and sent to Parse as part of a save operation.
+   * 
+   * @param objectEncoder
+   *          An object responsible for serializing ParseObjects.
+   * @return An object to be jsonified.
+   */
+  Object encode(ParseEncoder objectEncoder) throws JSONException;
+
+  /**
+   * Writes the ParseFieldOperation to the given Parcel using the given encoder.
+   *
+   * @param dest
+   *          The destination Parcel.
+   * @param parcelableEncoder
+   *          A ParseParcelableEncoder.
+   */
+  void encode(Parcel dest, ParseParcelEncoder parcelableEncoder);
+
+  /**
+   * Returns a field operation that is composed of a previous operation followed by this operation.
+   * This will not mutate either operation. However, it may return self if the current operation is
+   * not affected by previous changes. For example:
+   * 
+   * 
+   * {increment by 2}.mergeWithPrevious({set to 5}) -> {set to 7}
+   * {set to 5}.mergeWithPrevious({increment by 2}) -> {set to 5}
+   * {add "foo"}.mergeWithPrevious({delete}) -> {set to ["foo"]}
+   * {delete}.mergeWithPrevious({add "foo"}) -> {delete}
+   * 
+ * + * @param previous + * The most recent operation on the field, or null if none. + * @return A new ParseFieldOperation or this. + */ + ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous); + + /** + * Returns a new estimated value based on a previous value and this operation. This value is not + * intended to be sent to Parse, but it used locally on the client to inspect the most likely + * current value for a field. The key and object are used solely for ParseRelation to be able to + * construct objects that refer back to its parent. + * + * @param oldValue + * The previous value for the field. + * @param key + * The key that this value is for. + * @return The new value for the field. + */ + Object apply(Object oldValue, String key); +} + diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperations.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperations.java new file mode 100644 index 0000000..7b25059 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFieldOperations.java @@ -0,0 +1,253 @@ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility methods to deal with {@link ParseFieldOperation} decoding, both from JSON objects and + * from {@link Parcel}s. + */ +/* package */ final class ParseFieldOperations { + private ParseFieldOperations() { + } + + /** + * A function that creates a ParseFieldOperation from a JSONObject or a Parcel. + */ + private interface ParseFieldOperationFactory { + ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException; + ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder); + } + + // A map of all known decoders. + private static Map opDecoderMap = new HashMap<>(); + + /** + * Registers a single factory for a given __op field value. + */ + private static void registerDecoder(String opName, ParseFieldOperationFactory factory) { + opDecoderMap.put(opName, factory); + } + + /** + * Registers a list of default decoder functions that convert a JSONObject with an __op field, + * or a Parcel with a op name string, into a ParseFieldOperation. + */ + static void registerDefaultDecoders() { + registerDecoder(ParseRelationOperation.OP_NAME_BATCH, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + ParseFieldOperation op = null; + JSONArray ops = object.getJSONArray("ops"); + for (int i = 0; i < ops.length(); ++i) { + ParseFieldOperation nextOp = ParseFieldOperations.decode(ops.getJSONObject(i), decoder); + op = nextOp.mergeWithPrevious(op); + } + return op; + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + // Decode AddRelation and then RemoveRelation + ParseFieldOperation add = ParseFieldOperations.decode(source, decoder); + ParseFieldOperation remove = ParseFieldOperations.decode(source, decoder); + return remove.mergeWithPrevious(add); + } + }); + + registerDecoder(ParseDeleteOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + return ParseDeleteOperation.getInstance(); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return ParseDeleteOperation.getInstance(); + } + }); + + registerDecoder(ParseIncrementOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + return new ParseIncrementOperation((Number) decoder.decode(object.opt("amount"))); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return new ParseIncrementOperation((Number) decoder.decode(source)); + } + }); + + registerDecoder(ParseAddOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + return new ParseAddOperation((Collection) decoder.decode(object.opt("objects"))); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(i, decoder.decode(source)); + } + return new ParseAddOperation(list); + } + }); + + registerDecoder(ParseAddUniqueOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + return new ParseAddUniqueOperation((Collection) decoder.decode(object.opt("objects"))); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(i, decoder.decode(source)); + } + return new ParseAddUniqueOperation(list); + } + }); + + registerDecoder(ParseRemoveOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + return new ParseRemoveOperation((Collection) decoder.decode(object.opt("objects"))); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(i, decoder.decode(source)); + } + return new ParseRemoveOperation(list); + } + }); + + registerDecoder(ParseRelationOperation.OP_NAME_ADD, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + JSONArray objectsArray = object.optJSONArray("objects"); + List objectsList = (List) decoder.decode(objectsArray); + return new ParseRelationOperation<>(new HashSet<>(objectsList), null); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(set, null); + } + }); + + registerDecoder(ParseRelationOperation.OP_NAME_REMOVE, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) + throws JSONException { + JSONArray objectsArray = object.optJSONArray("objects"); + List objectsList = (List) decoder.decode(objectsArray); + return new ParseRelationOperation<>(null, new HashSet<>(objectsList)); + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(null, set); + } + }); + + registerDecoder(ParseSetOperation.OP_NAME, new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { + return null; // Not called. + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + return new ParseSetOperation(decoder.decode(source)); + } + }); + } + + /** + * Converts a parsed JSON object into a ParseFieldOperation. + * + * @param encoded + * A JSONObject containing an __op field. + * @return A ParseFieldOperation. + */ + static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) throws JSONException { + String op = encoded.optString("__op"); + ParseFieldOperationFactory factory = opDecoderMap.get(op); + if (factory == null) { + throw new RuntimeException("Unable to decode operation of type " + op); + } + return factory.decode(encoded, decoder); + } + + /** + * Reads a ParseFieldOperation out of the given Parcel. + * + * @param source + * The source Parcel. + * @param decoder + * The given ParseParcelableDecoder. + * + * @return A ParseFieldOperation. + */ + static ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { + String op = source.readString(); + ParseFieldOperationFactory factory = opDecoderMap.get(op); + if (factory == null) { + throw new RuntimeException("Unable to decode operation of type " + op); + } + return factory.decode(source, decoder); + } + + /** + * Converts a JSONArray into an ArrayList. + */ + static ArrayList jsonArrayAsArrayList(JSONArray array) { + ArrayList result = new ArrayList<>(array.length()); + for (int i = 0; i < array.length(); ++i) { + try { + result.add(array.get(i)); + } catch (JSONException e) { + // This can't actually happen. + throw new RuntimeException(e); + } + } + return result; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFile.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFile.java new file mode 100644 index 0000000..f2dac8a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFile.java @@ -0,0 +1,774 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * {@code ParseFile} is a local representation of a file that is saved to the Parse cloud. + *

+ * The workflow is to construct a {@code ParseFile} with data and optionally a filename. Then save + * it and set it as a field on a {@link ParseObject}. + *

+ * Example: + *

+ * ParseFile file = new ParseFile("hello".getBytes());
+ * file.save();
+ *
+ * ParseObject object = new ParseObject("TestObject");
+ * object.put("file", file);
+ * object.save();
+ * 
+ */ +public class ParseFile implements Parcelable { + + /* package for tests */ static ParseFileController getFileController() { + return ParseCorePlugins.getInstance().getFileController(); + } + + private static ProgressCallback progressCallbackOnMainThread( + final ProgressCallback progressCallback) { + if (progressCallback == null) { + return null; + } + + return new ProgressCallback() { + @Override + public void done(final Integer percentDone) { + Task.call(new Callable() { + @Override + public Void call() throws Exception { + progressCallback.done(percentDone); + return null; + } + }, ParseExecutors.main()); + } + }; + } + + /* package */ static class State { + + /* package */ static class Builder { + + private String name; + private String mimeType; + private String url; + + public Builder() { + // do nothing + } + + public Builder(State state) { + name = state.name(); + mimeType = state.mimeType(); + url = state.url(); + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder mimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + + public State build() { + return new State(this); + } + } + + private final String name; + private final String contentType; + private final String url; + + private State(Builder builder) { + name = builder.name != null ? builder.name : "file"; + contentType = builder.mimeType; + url = builder.url; + } + + public String name() { + return name; + } + + public String mimeType() { + return contentType; + } + + public String url() { + return url; + } + } + + private State state; + + /** + * Staging of {@code ParseFile}'s data is stored in memory until the {@code ParseFile} has been + * successfully synced with the server. + */ + /* package for tests */ byte[] data; + /* package for tests */ File file; + + /* package for tests */ final TaskQueue taskQueue = new TaskQueue(); + private Set> currentTasks = Collections.synchronizedSet( + new HashSet>()); + + /** + * Creates a new file from a file pointer. + * + * @param file + * The file. + */ + public ParseFile(File file) { + this(file, null); + } + + /** + * Creates a new file from a file pointer, and content type. Content type will be used instead of + * auto-detection by file extension. + * + * @param file + * The file. + * @param contentType + * The file's content type. + */ + public ParseFile(File file, String contentType) { + this(new State.Builder().name(file.getName()).mimeType(contentType).build()); + this.file = file; + } + + /** + * Creates a new file from a byte array, file name, and content type. Content type will be used + * instead of auto-detection by file extension. + * + * @param name + * The file's name, ideally with extension. The file name must begin with an alphanumeric + * character, and consist of alphanumeric characters, periods, spaces, underscores, or + * dashes. + * @param data + * The file's data. + * @param contentType + * The file's content type. + */ + public ParseFile(String name, byte[] data, String contentType) { + this(new State.Builder().name(name).mimeType(contentType).build()); + this.data = data; + } + + /** + * Creates a new file from a byte array. + * + * @param data + * The file's data. + */ + public ParseFile(byte[] data) { + this(null, data, null); + } + + /** + * Creates a new file from a byte array and a name. Giving a name with a proper file extension + * (e.g. ".png") is ideal because it allows Parse to deduce the content type of the file and set + * appropriate HTTP headers when it is fetched. + * + * @param name + * The file's name, ideally with extension. The file name must begin with an alphanumeric + * character, and consist of alphanumeric characters, periods, spaces, underscores, or + * dashes. + * @param data + * The file's data. + */ + public ParseFile(String name, byte[] data) { + this(name, data, null); + } + + /** + * Creates a new file from a byte array, and content type. Content type will be used instead of + * auto-detection by file extension. + * + * @param data + * The file's data. + * @param contentType + * The file's content type. + */ + public ParseFile(byte[] data, String contentType) { + this(null, data, contentType); + } + + /** + * Creates a new file instance from a {@link Parcel} source. This is used when unparceling + * a non-dirty ParseFile. Subclasses that need Parcelable behavior should provide their own + * {@link android.os.Parcelable.Creator} and override this constructor. + * + * @param source + * the source Parcel + */ + protected ParseFile(Parcel source) { + this(source, ParseParcelDecoder.get()); + } + + /** + * Creates a new file instance from a {@link Parcel} using the given {@link ParseParcelDecoder}. + * The decoder is currently unused, but it might be in the future, plus this is the pattern we + * are using in parcelable classes. + * + * @param source the parcel + * @param decoder the decoder + */ + ParseFile(Parcel source, ParseParcelDecoder decoder) { + this(new State.Builder() + .url(source.readString()) + .name(source.readString()) + .mimeType(source.readByte() == 1 ? source.readString() : null) + .build()); + } + + /* package for tests */ ParseFile(State state) { + this.state = state; + } + + /* package for tests */ State getState() { + return state; + } + + /** + * The filename. Before save is called, this is just the filename given by the user (if any). + * After save is called, that name gets prefixed with a unique identifier. + * + * @return The file's name. + */ + public String getName() { + return state.name(); + } + + /** + * Whether the file still needs to be saved. + * + * @return Whether the file needs to be saved. + */ + public boolean isDirty() { + return state.url() == null; + } + + /** + * Whether the file has available data. + */ + public boolean isDataAvailable() { + return data != null || getFileController().isDataAvailable(state); + } + + /** + * This returns the url of the file. It's only available after you save or after you get the file + * from a ParseObject. + * + * @return The url of the file. + */ + public String getUrl() { + return state.url(); + } + + /** + * Saves the file to the Parse cloud synchronously. + */ + public void save() throws ParseException { + ParseTaskUtils.wait(saveInBackground()); + } + + private Task saveAsync(final String sessionToken, + final ProgressCallback uploadProgressCallback, + Task toAwait, final Task cancellationToken) { + // If the file isn't dirty, just return immediately. + if (!isDirty()) { + return Task.forResult(null); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + // Wait for our turn in the queue, then check state to decide whether to no-op. + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!isDirty()) { + return Task.forResult(null); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + Task saveTask; + if (data != null) { + saveTask = getFileController().saveAsync( + state, + data, + sessionToken, + progressCallbackOnMainThread(uploadProgressCallback), + cancellationToken); + } else { + saveTask = getFileController().saveAsync( + state, + file, + sessionToken, + progressCallbackOnMainThread(uploadProgressCallback), + cancellationToken); + } + + return saveTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + state = task.getResult(); + // Since we have successfully uploaded the file, we do not need to hold the file pointer + // anymore. + data = null; + file = null; + return task.makeVoid(); + } + }); + } + }); + } + + /** + * Saves the file to the Parse cloud in a background thread. + * `progressCallback` is guaranteed to be called with 100 before saveCallback is called. + * + * @param uploadProgressCallback + * A ProgressCallback that is called periodically with progress updates. + * @return A Task that will be resolved when the save completes. + */ + public Task saveInBackground(final ProgressCallback uploadProgressCallback) { + final TaskCompletionSource cts = new TaskCompletionSource<>(); + currentTasks.add(cts); + + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return saveAsync(sessionToken, uploadProgressCallback, cts.getTask()); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; + } + }); + } + + /* package */ Task saveAsync(final String sessionToken, + final ProgressCallback uploadProgressCallback, final Task cancellationToken) { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken); + } + }); + } + + /** + * Saves the file to the Parse cloud in a background thread. + * + * @return A Task that will be resolved when the save completes. + */ + public Task saveInBackground() { + return saveInBackground((ProgressCallback) null); + } + + /** + * Saves the file to the Parse cloud in a background thread. + * `progressCallback` is guaranteed to be called with 100 before saveCallback is called. + * + * @param saveCallback + * A SaveCallback that gets called when the save completes. + * @param progressCallback + * A ProgressCallback that is called periodically with progress updates. + */ + public void saveInBackground(final SaveCallback saveCallback, + ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(progressCallback), saveCallback); + } + + /** + * Saves the file to the Parse cloud in a background thread. + * + * @param callback + * A SaveCallback that gets called when the save completes. + */ + public void saveInBackground(SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback); + } + + /** + * Synchronously gets the data from cache if available or fetches its content from the network. + * You probably want to use {@link #getDataInBackground()} instead unless you're already in a + * background thread. + */ + public byte[] getData() throws ParseException { + return ParseTaskUtils.wait(getDataInBackground()); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code ProgressCallback} will be called periodically with progress updates. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the data has been fetched. + */ + public Task getDataInBackground(final ProgressCallback progressCallback) { + final TaskCompletionSource cts = new TaskCompletionSource<>(); + currentTasks.add(cts); + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { + @Override + public byte[] then(Task task) throws Exception { + File file = task.getResult(); + try { + return ParseFileUtils.readFileToByteArray(file); + } catch (IOException e) { + // do nothing + } + return null; + } + }); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; + } + }); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * + * @return A Task that is resolved when the data has been fetched. + */ + public Task getDataInBackground() { + return getDataInBackground((ProgressCallback) null); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code ProgressCallback} will be called periodically with progress updates. + * A {@code GetDataCallback} will be called when the get completes. + * + * @param dataCallback + * A {@code GetDataCallback} that is called when the get completes. + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + */ + public void getDataInBackground(GetDataCallback dataCallback, + final ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(progressCallback), dataCallback); + } + + /** + * Asynchronously gets the data from cache if available or fetches its content from the network. + * A {@code GetDataCallback} will be called when the get completes. + * + * @param dataCallback + * A {@code GetDataCallback} that is called when the get completes. + */ + public void getDataInBackground(GetDataCallback dataCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataInBackground(), dataCallback); + } + + /** + * Synchronously gets the file pointer from cache if available or fetches its content from the + * network. You probably want to use {@link #getFileInBackground()} instead unless you're already + * in a background thread. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + */ + public File getFile() throws ParseException { + return ParseTaskUtils.wait(getFileInBackground()); + } + + /** + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code ProgressCallback} will be called periodically with progress updates. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the file pointer of this object has been fetched. + */ + public Task getFileInBackground(final ProgressCallback progressCallback) { + final TaskCompletionSource cts = new TaskCompletionSource<>(); + currentTasks.add(cts); + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return fetchInBackground(progressCallback, toAwait, cts.getTask()); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; + } + }); + } + + /** + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + * + * @return A Task that is resolved when the data has been fetched. + */ + public Task getFileInBackground() { + return getFileInBackground((ProgressCallback) null); + } + + /** + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code GetFileCallback} will be called when the get completes. + * The {@code ProgressCallback} will be called periodically with progress updates. + * The {@code ProgressCallback} is guaranteed to be called with 100 before the + * {@code GetFileCallback} is called. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + * + * @param fileCallback + * A {@code GetFileCallback} that is called when the get completes. + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + */ + public void getFileInBackground(GetFileCallback fileCallback, + final ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(progressCallback), fileCallback); + } + + /** + * Asynchronously gets the file pointer from cache if available or fetches its content from the + * network. The {@code GetFileCallback} will be called when the get completes. + * Note: The {@link File} location may change without notice and should not be + * stored to be accessed later. + * + * @param fileCallback + * A {@code GetFileCallback} that is called when the get completes. + */ + public void getFileInBackground(GetFileCallback fileCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getFileInBackground(), fileCallback); + } + + /** + * Synchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * You probably want to use {@link #getDataStreamInBackground} instead unless you're already in a + * background thread. + */ + public InputStream getDataStream() throws ParseException { + return ParseTaskUtils.wait(getDataStreamInBackground()); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code ProgressCallback} will be called periodically with progress updates. + * + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + * @return A Task that is resolved when the data stream of this object has been fetched. + */ + public Task getDataStreamInBackground(final ProgressCallback progressCallback) { + final TaskCompletionSource cts = new TaskCompletionSource<>(); + currentTasks.add(cts); + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { + @Override + public InputStream then(Task task) throws Exception { + return new FileInputStream(task.getResult()); + } + }); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; + } + }); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * + * @return A Task that is resolved when the data stream has been fetched. + */ + public Task getDataStreamInBackground() { + return getDataStreamInBackground((ProgressCallback) null); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code GetDataStreamCallback} will be called when the get completes. The + * {@code ProgressCallback} will be called periodically with progress updates. The + * {@code ProgressCallback} is guaranteed to be called with 100 before + * {@code GetDataStreamCallback} is called. + * + * @param dataStreamCallback + * A {@code GetDataStreamCallback} that is called when the get completes. + * @param progressCallback + * A {@code ProgressCallback} that is called periodically with progress updates. + */ + public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback, + final ProgressCallback progressCallback) { + ParseTaskUtils.callbackOnMainThreadAsync( + getDataStreamInBackground(progressCallback), dataStreamCallback); + } + + /** + * Asynchronously gets the data stream from cached file if available or fetches its content from + * the network, saves the content as cached file and returns the data stream of the cached file. + * The {@code GetDataStreamCallback} will be called when the get completes. + * + * @param dataStreamCallback + * A {@code GetDataStreamCallback} that is called when the get completes. + */ + public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback) { + ParseTaskUtils.callbackOnMainThreadAsync(getDataStreamInBackground(), dataStreamCallback); + } + + private Task fetchInBackground( + final ProgressCallback progressCallback, + Task toAwait, + final Task cancellationToken) { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + return getFileController().fetchAsync( + state, + null, + progressCallbackOnMainThread(progressCallback), + cancellationToken); + } + }); + } + + /** + * Cancels the operations for this {@code ParseFile} if they are still in the task queue. However, + * if a network request has already been started for an operation, the network request will not + * be canceled. + */ + //TODO (grantland): Deprecate and replace with CancellationToken + public void cancel() { + Set> tasks = new HashSet<>(currentTasks); + for (TaskCompletionSource tcs : tasks) { + tcs.trySetCancelled(); + } + currentTasks.removeAll(tasks); + } + + /* + * Encode/Decode + */ + @SuppressWarnings("unused") + /* package */ ParseFile(JSONObject json, ParseDecoder decoder) { + this(new State.Builder().name(json.optString("name")).url(json.optString("url")).build()); + } + + /* package */ JSONObject encode() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "File"); + json.put("name", getName()); + + String url = getUrl(); + if (url == null) { + throw new IllegalStateException("Unable to encode an unsaved ParseFile."); + } + json.put("url", getUrl()); + + return json; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, ParseParcelEncoder.get()); + } + + void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + if (isDirty()) { + throw new RuntimeException("Unable to parcel an unsaved ParseFile."); + } + dest.writeString(getUrl()); // Not null + dest.writeString(getName()); // Not null + String type = state.mimeType(); // Nullable + dest.writeByte(type != null ? (byte) 1 : 0); + if (type != null) { + dest.writeString(type); + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseFile createFromParcel(Parcel source) { + return new ParseFile(source); + } + + @Override + public ParseFile[] newArray(int size) { + return new ParseFile[size]; + } + }; +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileController.java new file mode 100644 index 0000000..8e9abe7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileController.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; + +import bolts.Continuation; +import bolts.Task; + +// TODO(grantland): Create ParseFileController interface +/** package */ class ParseFileController { + + private final Object lock = new Object(); + private final ParseHttpClient restClient; + private final File cachePath; + + private ParseHttpClient fileClient; + + public ParseFileController(ParseHttpClient restClient, File cachePath) { + this.restClient = restClient; + this.cachePath = cachePath; + } + + /** + * Gets the file http client if exists, otherwise lazily creates since developers might not always + * use our download mechanism. + */ + /* package */ ParseHttpClient fileClient() { + synchronized (lock) { + if (fileClient == null) { + fileClient = ParsePlugins.get().fileClient(); + } + return fileClient; + } + } + + /* package for tests */ ParseFileController fileClient(ParseHttpClient fileClient) { + synchronized (lock) { + this.fileClient = fileClient; + } + return this; + } + + public File getCacheFile(ParseFile.State state) { + return new File(cachePath, state.name()); + } + + /* package for tests */ File getTempFile(ParseFile.State state) { + if (state.url() == null) { + return null; + } + return new File(cachePath, state.url() + ".tmp"); + } + + public boolean isDataAvailable(ParseFile.State state) { + return getCacheFile(state).exists(); + } + + public void clearCache() { + File[] files = cachePath.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + ParseFileUtils.deleteQuietly(file); + } + } + + public Task saveAsync( + final ParseFile.State state, + final byte[] data, + String sessionToken, + ProgressCallback uploadProgressCallback, + Task cancellationToken) { + if (state.url() != null) { // !isDirty + return Task.forResult(state); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + final ParseRESTCommand command = new ParseRESTFileCommand.Builder() + .fileName(state.name()) + .data(data) + .contentType(state.mimeType()) + .sessionToken(sessionToken) + .build(); + + return command.executeAsync( + restClient, + uploadProgressCallback, + null, + cancellationToken + ).onSuccess(new Continuation() { + @Override + public ParseFile.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + ParseFile.State newState = new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.writeByteArrayToFile(getCacheFile(newState), data); + } catch (IOException e) { + // do nothing + } + + return newState; + } + }, ParseExecutors.io()); + } + + public Task saveAsync( + final ParseFile.State state, + final File file, + String sessionToken, + ProgressCallback uploadProgressCallback, + Task cancellationToken) { + if (state.url() != null) { // !isDirty + return Task.forResult(state); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + final ParseRESTCommand command = new ParseRESTFileCommand.Builder() + .fileName(state.name()) + .file(file) + .contentType(state.mimeType()) + .sessionToken(sessionToken) + .build(); + + return command.executeAsync( + restClient, + uploadProgressCallback, + null, + cancellationToken + ).onSuccess(new Continuation() { + @Override + public ParseFile.State then(Task task) throws Exception { + JSONObject result = task.getResult(); + ParseFile.State newState = new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.copyFile(file, getCacheFile(newState)); + } catch (IOException e) { + // do nothing + } + + return newState; + } + }, ParseExecutors.io()); + } + + public Task fetchAsync( + final ParseFile.State state, + @SuppressWarnings("UnusedParameters") String sessionToken, + final ProgressCallback downloadProgressCallback, + final Task cancellationToken) { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + final File cacheFile = getCacheFile(state); + return Task.call(new Callable() { + @Override + public Boolean call() throws Exception { + return cacheFile.exists(); + } + }, ParseExecutors.io()).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + boolean result = task.getResult(); + if (result) { + return Task.forResult(cacheFile); + } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + // Generate the temp file path for caching ParseFile content based on ParseFile's url + // The reason we do not write to the cacheFile directly is because there is no way we can + // verify if a cacheFile is complete or not. If download is interrupted in the middle, next + // time when we download the ParseFile, since cacheFile has already existed, we will return + // this incomplete cacheFile + final File tempFile = getTempFile(state); + + // network + final ParseFileRequest request = + new ParseFileRequest(ParseHttpRequest.Method.GET, state.url(), tempFile); + + // We do not need to delete the temp file since we always try to overwrite it + return request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // If the top-level task was cancelled, don't actually set the data -- just move on. + if (cancellationToken != null && cancellationToken.isCancelled()) { + throw new CancellationException(); + } + if (task.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + return task.cast(); + } + + // Since we give the cacheFile pointer to developers, it is not safe to guarantee + // cacheFile always does not exist here, so it is better to delete it manually, + // otherwise moveFile may throw an exception. + ParseFileUtils.deleteQuietly(cacheFile); + ParseFileUtils.moveFile(tempFile, cacheFile); + return Task.forResult(cacheFile); + } + }, ParseExecutors.io()); + } + }); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileHttpBody.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileHttpBody.java new file mode 100644 index 0000000..378c7d9 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileHttpBody.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpBody; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** package */ class ParseFileHttpBody extends ParseHttpBody { + + /* package */ final File file; + + public ParseFileHttpBody(File file) { + this(file, null); + } + + public ParseFileHttpBody(File file, String contentType) { + super(contentType, file.length()); + this.file = file; + } + + @Override + public InputStream getContent() throws IOException { + return new FileInputStream(file); + } + + @Override + public void writeTo(OutputStream out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("Output stream can not be null"); + } + + final FileInputStream fileInput = new FileInputStream(file); + try { + ParseIOUtils.copy(fileInput, out); + } finally { + ParseIOUtils.closeQuietly(fileInput); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileRequest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileRequest.java new file mode 100644 index 0000000..f5d9494 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileRequest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.concurrent.Callable; + +import bolts.Task; + +/** + * Request returns a byte array of the response and provides a callback the progress of the data + * read from the network. + */ +/** package */ class ParseFileRequest extends ParseRequest { + + // The temp file is used to save the ParseFile content when we fetch it from server + private final File tempFile; + + public ParseFileRequest(ParseHttpRequest.Method method, String url, File tempFile) { + super(method, url); + this.tempFile = tempFile; + } + + @Override + protected Task onResponseAsync(final ParseHttpResponse response, + final ProgressCallback downloadProgressCallback) { + int statusCode = response.getStatusCode(); + if (statusCode >= 200 && statusCode < 300 || statusCode == 304) { + // OK + } else { + String action = method == ParseHttpRequest.Method.GET ? "Download from" : "Upload to"; + return Task.forError(new ParseException(ParseException.CONNECTION_FAILED, String.format( + "%s file server failed. %s", action, response.getReasonPhrase()))); + } + + if (method != ParseHttpRequest.Method.GET) { + return null; + } + + return Task.call(new Callable() { + @Override + public Void call() throws Exception { + long totalSize = response.getTotalSize(); + long downloadedSize = 0; + InputStream responseStream = null; + FileOutputStream tempFileStream = null; + try { + responseStream = response.getContent(); + tempFileStream = ParseFileUtils.openOutputStream(tempFile); + + int nRead; + byte[] data = new byte[32 << 10]; // 32KB + + while ((nRead = responseStream.read(data, 0, data.length)) != -1) { + tempFileStream.write(data, 0, nRead); + downloadedSize += nRead; + if (downloadProgressCallback != null && totalSize != -1) { + int progressToReport = + Math.round((float) downloadedSize / (float) totalSize * 100.0f); + downloadProgressCallback.done(progressToReport); + } + } + return null; + } finally { + ParseIOUtils.closeQuietly(responseStream); + ParseIOUtils.closeQuietly(tempFileStream); + } + } + }, ParseExecutors.io()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileUtils.java new file mode 100644 index 0000000..7510b1a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseFileUtils.java @@ -0,0 +1,546 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +/** + * General file manipulation utilities. + */ +/** package */ class ParseFileUtils { + + /** + * The number of bytes in a kilobyte. + */ + public static final long ONE_KB = 1024; + + /** + * The number of bytes in a megabyte. + */ + public static final long ONE_MB = ONE_KB * ONE_KB; + + /** + * The file copy buffer size (30 MB) + */ + private static final long FILE_COPY_BUFFER_SIZE = ONE_MB * 30; + + /** + * Reads the contents of a file into a byte array. + * The file is always closed. + * + * @param file the file to read, must not be null + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @since Commons IO 1.1 + */ + public static byte[] readFileToByteArray(File file) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return ParseIOUtils.toByteArray(in); + } finally { + ParseIOUtils.closeQuietly(in); + } + } + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileInputStream} for the specified file, providing better + * error messages than simply calling new FileInputStream(file). + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * An exception is thrown if the file does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be read. + * + * @param file the file to open for input, must not be null + * @return a new {@link FileInputStream} for the specified file + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be read + * @since Commons IO 1.3 + */ + public static FileInputStream openInputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (!file.canRead()) { + throw new IOException("File '" + file + "' cannot be read"); + } + } else { + throw new FileNotFoundException("File '" + file + "' does not exist"); + } + return new FileInputStream(file); + } + + /** + * Writes a byte array to a file creating the file if it does not exist. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @since Commons IO 1.1 + */ + public static void writeByteArrayToFile(File file, byte[] data) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file); + out.write(data); + } finally { + ParseIOUtils.closeQuietly(out); + } + } + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be null + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since Commons IO 1.3 + */ + public static FileOutputStream openOutputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (!file.canWrite()) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + if (!parent.mkdirs()) { + throw new IOException("File '" + file + "' could not be created"); + } + } + } + return new FileOutputStream(file); + } + + /** + * Moves a file. + *

+ * When the destination file is on another file system, do a "copy and delete". + * + * @param srcFile the file to be moved + * @param destFile the destination file + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveFile(final File srcFile, final File destFile) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' is a directory"); + } + if (destFile.exists()) { + throw new IOException("Destination '" + destFile + "' already exists"); + } + if (destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' is a directory"); + } + final boolean rename = srcFile.renameTo(destFile); + if (!rename) { + copyFile( srcFile, destFile ); + if (!srcFile.delete()) { + ParseFileUtils.deleteQuietly(destFile); + throw new IOException("Failed to delete original file '" + srcFile + + "' after copy to '" + destFile + "'"); + } + } + } + + /** + * Copies a file to a new location preserving the file date. + *

+ * This method copies the contents of the specified source file to the + * specified destination file. The directory holding the destination file is + * created if it does not exist. If the destination file exists, then this + * method will overwrite it. + *

+ * Note: This method tries to preserve the file's last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destFile the new file, must not be {@code null} + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @throws IOException if the output file length is not the same as the input file length after the copy completes + * @see #copyFile(File, File, boolean) + */ + public static void copyFile(final File srcFile, final File destFile) throws IOException { + copyFile(srcFile, destFile, true); + } + + /** + * Copies a file to a new location. + *

+ * This method copies the contents of the specified source file + * to the specified destination file. + * The directory holding the destination file is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + *

+ * Note: Setting preserveFileDate to + * {@code true} tries to preserve the file's last modified + * date/times using {@link File#setLastModified(long)}, however it is + * not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destFile the new file, must not be {@code null} + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @throws IOException if the output file length is not the same as the input file length after the copy completes + * @see #doCopyFile(File, File, boolean) + */ + public static void copyFile(final File srcFile, final File destFile, + final boolean preserveFileDate) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' exists but is a directory"); + } + if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { + throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same"); + } + final File parentFile = destFile.getParentFile(); + if (parentFile != null) { + if (!parentFile.mkdirs() && !parentFile.isDirectory()) { + throw new IOException("Destination '" + parentFile + "' directory cannot be created"); + } + } + if (destFile.exists() && !destFile.canWrite()) { + throw new IOException("Destination '" + destFile + "' exists but is read-only"); + } + doCopyFile(srcFile, destFile, preserveFileDate); + } + + /** + * Internal copy file method. + * This caches the original file length, and throws an IOException + * if the output file length is different from the current input file length. + * So it may fail if the file changes size. + * It may also fail with "IllegalArgumentException: Negative size" if the input file is truncated part way + * through copying the data and the new file size is less than the current position. + * + * @param srcFile the validated source file, must not be {@code null} + * @param destFile the validated destination file, must not be {@code null} + * @param preserveFileDate whether to preserve the file date + * @throws IOException if an error occurs + * @throws IOException if the output file length is not the same as the input file length after the copy completes + * @throws IllegalArgumentException "Negative size" if the file is truncated so that the size is less than the position + */ + private static void doCopyFile(final File srcFile, final File destFile, final boolean preserveFileDate) throws IOException { + if (destFile.exists() && destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' exists but is a directory"); + } + + FileInputStream fis = null; + FileOutputStream fos = null; + FileChannel input = null; + FileChannel output = null; + try { + fis = new FileInputStream(srcFile); + fos = new FileOutputStream(destFile); + input = fis.getChannel(); + output = fos.getChannel(); + final long size = input.size(); // TODO See IO-386 + long pos = 0; + long count = 0; + while (pos < size) { + final long remain = size - pos; + count = remain > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : remain; + final long bytesCopied = output.transferFrom(input, pos, count); + if (bytesCopied == 0) { // IO-385 - can happen if file is truncated after caching the size + break; // ensure we don't loop forever + } + pos += bytesCopied; + } + } finally { + ParseIOUtils.closeQuietly(output); + ParseIOUtils.closeQuietly(fos); + ParseIOUtils.closeQuietly(input); + ParseIOUtils.closeQuietly(fis); + } + + final long srcLen = srcFile.length(); // TODO See IO-386 + final long dstLen = destFile.length(); // TODO See IO-386 + if (srcLen != dstLen) { + throw new IOException("Failed to copy full contents from '" + + srcFile + "' to '" + destFile + "' Expected length: " + srcLen +" Actual: " + dstLen); + } + if (preserveFileDate) { + destFile.setLastModified(srcFile.lastModified()); + } + } + + //----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(final File directory) throws IOException { + if (!directory.exists()) { + return; + } + + if (!isSymlink(directory)) { + cleanDirectory(directory); + } + + if (!directory.delete()) { + final String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • No exceptions are thrown when a file or directory cannot be deleted.
  • + *
+ * + * @param file file or directory to delete, can be {@code null} + * @return {@code true} if the file or directory was deleted, otherwise + * {@code false} + * + * @since 1.4 + */ + public static boolean deleteQuietly(final File file) { + if (file == null) { + return false; + } + try { + if (file.isDirectory()) { + cleanDirectory(file); + } + } catch (final Exception ignored) { + } + + try { + return file.delete(); + } catch (final Exception ignored) { + return false; + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + */ + public static void cleanDirectory(final File directory) throws IOException { + if (!directory.exists()) { + final String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + final String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + final File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (final File file : files) { + try { + forceDelete(file); + } catch (final IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + //----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • You get exceptions when a file or directory cannot be deleted. + * (java.io.File methods returns a boolean)
  • + *
+ * + * @param file file or directory to delete, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(final File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + final boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent){ + throw new FileNotFoundException("File does not exist: " + file); + } + final String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Determines whether the specified file is a Symbolic Link rather than an actual file. + *

+ * Will not return true if there is a Symbolic Link anywhere in the path, + * only if the specific file is. + *

+ * For code that runs on Java 1.7 or later, use the following method instead: + *
+ * {@code boolean java.nio.file.Files.isSymbolicLink(Path path)} + * @param file the file to check + * @return true if the file is a Symbolic Link + * @throws IOException if an IO error occurs while checking the file + * @since 2.0 + */ + public static boolean isSymlink(final File file) throws IOException { + if (file == null) { + throw new NullPointerException("File must not be null"); + } +// if (FilenameUtils.isSystemWindows()) { +// return false; +// } + File fileInCanonicalDir = null; + if (file.getParent() == null) { + fileInCanonicalDir = file; + } else { + final File canonicalDir = file.getParentFile().getCanonicalFile(); + fileInCanonicalDir = new File(canonicalDir, file.getName()); + } + + if (fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile())) { + return false; + } else { + return true; + } + } + + //region String + + public static String readFileToString(File file, Charset encoding) throws IOException { + return new String(readFileToByteArray(file), encoding); + } + + public static String readFileToString(File file, String encoding) throws IOException { + return readFileToString(file, Charset.forName(encoding)); + } + + public static void writeStringToFile(File file, String string, Charset encoding) + throws IOException { + writeByteArrayToFile(file, string.getBytes(encoding)); + } + + public static void writeStringToFile(File file, String string, String encoding) + throws IOException { + writeStringToFile(file, string, Charset.forName(encoding)); + } + + //endregion + + //region JSONObject + + /** + * Reads the contents of a file into a {@link JSONObject}. The file is always closed. + */ + public static JSONObject readFileToJSONObject(File file) throws IOException, JSONException { + String content = readFileToString(file, "UTF-8"); + return new JSONObject(content); + } + + /** + * Writes a {@link JSONObject} to a file creating the file if it does not exist. + */ + public static void writeJSONObjectToFile(File file, JSONObject json) throws IOException { + ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes(Charset.forName("UTF-8"))); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseGeoPoint.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseGeoPoint.java new file mode 100644 index 0000000..4f901d5 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseGeoPoint.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.location.Criteria; +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +import bolts.Continuation; +import bolts.Task; + +/** + * {@code ParseGeoPoint} represents a latitude / longitude point that may be associated with a key + * in a {@link ParseObject} or used as a reference point for geo queries. This allows proximity + * based queries on the key. + *

+ * Only one key in a class may contain a {@code ParseGeoPoint}. + *

+ * Example: + *

+ * ParseGeoPoint point = new ParseGeoPoint(30.0, -20.0);
+ * ParseObject object = new ParseObject("PlaceObject");
+ * object.put("location", point);
+ * object.save();
+ * 
+ */ + +public class ParseGeoPoint implements Parcelable { + static double EARTH_MEAN_RADIUS_KM = 6371.0; + static double EARTH_MEAN_RADIUS_MILE = 3958.8; + + private double latitude = 0.0; + private double longitude = 0.0; + + /** + * Creates a new default point with latitude and longitude set to 0.0. + */ + public ParseGeoPoint() { + } + + /** + * Creates a new point with the specified latitude and longitude. + * + * @param latitude + * The point's latitude. + * @param longitude + * The point's longitude. + */ + public ParseGeoPoint(double latitude, double longitude) { + setLatitude(latitude); + setLongitude(longitude); + } + + /** + * Creates a copy of {@code point}; + * + * @param point + * The point to copy. + */ + public ParseGeoPoint(ParseGeoPoint point) { + this(point.getLatitude(), point.getLongitude()); + } + + /** + * Creates a new point instance from a {@link Parcel} source. This is used when unparceling a + * ParseGeoPoint. Subclasses that need Parcelable behavior should provide their own + * {@link android.os.Parcelable.Creator} and override this constructor. + * + * @param source The recovered parcel. + */ + protected ParseGeoPoint(Parcel source) { + this(source, ParseParcelDecoder.get()); + } + + /** + * Creates a new point instance from a {@link Parcel} using the given {@link ParseParcelDecoder}. + * The decoder is currently unused, but it might be in the future, plus this is the pattern we + * are using in parcelable classes. + * + * @param source the parcel + * @param decoder the decoder + */ + ParseGeoPoint(Parcel source, ParseParcelDecoder decoder) { + setLatitude(source.readDouble()); + setLongitude(source.readDouble()); + } + + /** + * Set latitude. Valid range is (-90.0, 90.0). Extremes should not be used. + * + * @param latitude + * The point's latitude. + */ + public void setLatitude(double latitude) { + if (latitude > 90.0 || latitude < -90.0) { + throw new IllegalArgumentException("Latitude must be within the range (-90.0, 90.0)."); + } + this.latitude = latitude; + } + + /** + * Get latitude. + */ + public double getLatitude() { + return latitude; + } + + /** + * Set longitude. Valid range is (-180.0, 180.0). Extremes should not be used. + * + * @param longitude + * The point's longitude. + */ + public void setLongitude(double longitude) { + if (longitude > 180.0 || longitude < -180.0) { + throw new IllegalArgumentException("Longitude must be within the range (-180.0, 180.0)."); + } + this.longitude = longitude; + } + + /** + * Get longitude. + */ + public double getLongitude() { + return longitude; + } + + /** + * Get distance in radians between this point and another {@code ParseGeoPoint}. This is the + * smallest angular distance between the two points. + * + * @param point + * {@code ParseGeoPoint} describing the other point being measured against. + */ + public double distanceInRadiansTo(ParseGeoPoint point) { + double d2r = Math.PI / 180.0; // radian conversion factor + double lat1rad = latitude * d2r; + double long1rad = longitude * d2r; + double lat2rad = point.getLatitude() * d2r; + double long2rad = point.getLongitude() * d2r; + double deltaLat = lat1rad - lat2rad; + double deltaLong = long1rad - long2rad; + double sinDeltaLatDiv2 = Math.sin(deltaLat / 2.); + double sinDeltaLongDiv2 = Math.sin(deltaLong / 2.); + // Square of half the straight line chord distance between both points. + // [0.0, 1.0] + double a = + sinDeltaLatDiv2 * sinDeltaLatDiv2 + Math.cos(lat1rad) * Math.cos(lat2rad) + * sinDeltaLongDiv2 * sinDeltaLongDiv2; + a = Math.min(1.0, a); + return 2. * Math.asin(Math.sqrt(a)); + } + + /** + * Get distance between this point and another {@code ParseGeoPoint} in kilometers. + * + * @param point + * {@code ParseGeoPoint} describing the other point being measured against. + */ + public double distanceInKilometersTo(ParseGeoPoint point) { + return distanceInRadiansTo(point) * EARTH_MEAN_RADIUS_KM; + } + + /** + * Get distance between this point and another {@code ParseGeoPoint} in kilometers. + * + * @param point + * {@code ParseGeoPoint} describing the other point being measured against. + */ + public double distanceInMilesTo(ParseGeoPoint point) { + return distanceInRadiansTo(point) * EARTH_MEAN_RADIUS_MILE; + } + + /** + * Asynchronously fetches the current location of the device. + * + * This will use a default {@link Criteria} with no accuracy or power requirements, which will + * generally result in slower, but more accurate location fixes. + *

+ * Note: If GPS is the best provider, it might not be able to locate the device + * at all and timeout. + * + * @param timeout + * The number of milliseconds to allow before timing out. + * @return A Task that is resolved when a location is found. + * + * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) + * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) + */ + public static Task getCurrentLocationInBackground(long timeout) { + Criteria criteria = new Criteria(); + criteria.setAccuracy(Criteria.NO_REQUIREMENT); + criteria.setPowerRequirement(Criteria.NO_REQUIREMENT); + return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria) + .onSuccess(new Continuation() { + @Override + public ParseGeoPoint then(Task task) throws Exception { + Location location = task.getResult(); + return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); + } + }); + } + + /** + * Asynchronously fetches the current location of the device. + * + * This will use a default {@link Criteria} with no accuracy or power requirements, which will + * generally result in slower, but more accurate location fixes. + *

+ * Note: If GPS is the best provider, it might not be able to locate the device + * at all and timeout. + * + * @param timeout + * The number of milliseconds to allow before timing out. + * @param callback + * callback.done(geoPoint, error) is called when a location is found. + * + * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) + * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) + */ + public static void getCurrentLocationInBackground(long timeout, LocationCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(getCurrentLocationInBackground(timeout), callback); + } + + /** + * Asynchronously fetches the current location of the device. + * + * This will request location updates from the best provider that match the given criteria + * and return the first location received. + * + * You can customize the criteria to meet your specific needs. + * * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer + * times for a fix. + * * For better battery efficiency and faster location fixes, you can set + * {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy. + * + * @param timeout + * The number of milliseconds to allow before timing out. + * @param criteria + * The application criteria for selecting a location provider. + * @return A Task that is resolved when a location is found. + * + * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) + * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) + */ + public static Task getCurrentLocationInBackground(long timeout, Criteria criteria) { + return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria) + .onSuccess(new Continuation() { + @Override + public ParseGeoPoint then(Task task) throws Exception { + Location location = task.getResult(); + return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); + } + }); + } + + /** + * Asynchronously fetches the current location of the device. + * + * This will request location updates from the best provider that match the given criteria + * and return the first location received. + * + * You can customize the criteria to meet your specific needs. + * * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer + * times for a fix. + * * For better battery efficiency and faster location fixes, you can set + * {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy. + * + * @param timeout + * The number of milliseconds to allow before timing out. + * @param criteria + * The application criteria for selecting a location provider. + * @param callback + * callback.done(geoPoint, error) is called when a location is found. + * + * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) + * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) + */ + public static void getCurrentLocationInBackground(long timeout, Criteria criteria, + LocationCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(getCurrentLocationInBackground(timeout, criteria), callback); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ParseGeoPoint)) { + return false; + } + if (obj == this) { + return true; + } + return ((ParseGeoPoint) obj).getLatitude() == latitude && + ((ParseGeoPoint) obj).getLongitude() == longitude; + } + + @Override + public String toString() { + return String.format(Locale.US, "ParseGeoPoint[%.6f,%.6f]", latitude, longitude); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, ParseParcelEncoder.get()); + } + + void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeDouble(latitude); + dest.writeDouble(longitude); + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseGeoPoint createFromParcel(Parcel source) { + return new ParseGeoPoint(source, ParseParcelDecoder.get()); + } + + @Override + public ParseGeoPoint[] newArray(int size) { + return new ParseGeoPoint[size]; + } + }; +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseHttpClient.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseHttpClient.java new file mode 100644 index 0000000..b33fda5 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseHttpClient.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.support.annotation.Nullable; + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.Call; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.BufferedSink; + +/** + * Internal http client which wraps an {@link OkHttpClient} + */ +class ParseHttpClient { + + static ParseHttpClient createClient(@Nullable OkHttpClient.Builder builder) { + return new ParseHttpClient(builder); + } + + private OkHttpClient okHttpClient; + private boolean hasExecuted; + + ParseHttpClient(@Nullable OkHttpClient.Builder builder) { + + if (builder == null) { + builder = new OkHttpClient.Builder(); + } + + okHttpClient = builder.build(); + } + + public final ParseHttpResponse execute(ParseHttpRequest request) throws IOException { + if (!hasExecuted) { + hasExecuted = true; + } + return executeInternal(request); + } + + /** + * Execute internal. Keep default protection for tests + * @param parseRequest request + * @return response + * @throws IOException exception + */ + ParseHttpResponse executeInternal(ParseHttpRequest parseRequest) throws IOException { + Request okHttpRequest = getRequest(parseRequest); + Call okHttpCall = okHttpClient.newCall(okHttpRequest); + + Response okHttpResponse = okHttpCall.execute(); + + return getResponse(okHttpResponse); + } + + ParseHttpResponse getResponse(Response okHttpResponse) + throws IOException { + // Status code + int statusCode = okHttpResponse.code(); + + // Content + InputStream content = okHttpResponse.body().byteStream(); + + // Total size + int totalSize = (int) okHttpResponse.body().contentLength(); + + // Reason phrase + String reasonPhrase = okHttpResponse.message(); + + // Headers + Map headers = new HashMap<>(); + for (String name : okHttpResponse.headers().names()) { + headers.put(name, okHttpResponse.header(name)); + } + + // Content type + String contentType = null; + ResponseBody body = okHttpResponse.body(); + if (body != null && body.contentType() != null) { + contentType = body.contentType().toString(); + } + + return new ParseHttpResponse.Builder() + .setStatusCode(statusCode) + .setContent(content) + .setTotalSize(totalSize) + .setReasonPhrase(reasonPhrase) + .setHeaders(headers) + .setContentType(contentType) + .build(); + } + + Request getRequest(ParseHttpRequest parseRequest) throws IOException { + Request.Builder okHttpRequestBuilder = new Request.Builder(); + ParseHttpRequest.Method method = parseRequest.getMethod(); + // Set method + switch (method) { + case GET: + okHttpRequestBuilder.get(); + break; + case DELETE: + case POST: + case PUT: + // Since we need to set body and method at the same time for DELETE, POST, PUT, we will do it in + // the following. + break; + default: + // This case will never be reached since we have already handled this case in + // ParseRequest.newRequest(). + throw new IllegalStateException("Unsupported http method " + method.toString()); + } + // Set url + okHttpRequestBuilder.url(parseRequest.getUrl()); + + // Set Header + Headers.Builder okHttpHeadersBuilder = new Headers.Builder(); + for (Map.Entry entry : parseRequest.getAllHeaders().entrySet()) { + okHttpHeadersBuilder.add(entry.getKey(), entry.getValue()); + } + // OkHttp automatically add gzip header so we do not need to deal with it + Headers okHttpHeaders = okHttpHeadersBuilder.build(); + okHttpRequestBuilder.headers(okHttpHeaders); + + // Set Body + ParseHttpBody parseBody = parseRequest.getBody(); + ParseOkHttpRequestBody okHttpRequestBody = null; + if (parseBody != null) { + okHttpRequestBody = new ParseOkHttpRequestBody(parseBody); + } + switch (method) { + case PUT: + okHttpRequestBuilder.put(okHttpRequestBody); + break; + case POST: + okHttpRequestBuilder.post(okHttpRequestBody); + break; + case DELETE: + okHttpRequestBuilder.delete(okHttpRequestBody); + } + return okHttpRequestBuilder.build(); + } + + private static class ParseOkHttpRequestBody extends RequestBody { + + private ParseHttpBody parseBody; + + ParseOkHttpRequestBody(ParseHttpBody parseBody) { + this.parseBody = parseBody; + } + + @Override + public long contentLength() throws IOException { + return parseBody.getContentLength(); + } + + @Override + public MediaType contentType() { + String contentType = parseBody.getContentType(); + return contentType == null ? null : MediaType.parse(parseBody.getContentType()); + } + + @Override + public void writeTo(BufferedSink bufferedSink) throws IOException { + parseBody.writeTo(bufferedSink.outputStream()); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIOUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIOUtils.java new file mode 100644 index 0000000..7e2dded --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIOUtils.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.parse; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * General IO stream manipulation utilities. + */ +/** package */ class ParseIOUtils { + + private static final int EOF = -1; + + /** + * The default buffer size ({@value}) to use for + * {@link #copyLarge(InputStream, OutputStream)} + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * The default buffer size to use for the skip() methods. + */ + private static final int SKIP_BUFFER_SIZE = 2048; + + // Allocated in the relevant skip method if necessary. + /* + * N.B. no need to synchronize these because: + * - we don't care if the buffer is created multiple times (the data is ignored) + * - we always use the same size buffer, so if it it is recreated it will still be OK + * (if the buffer size were variable, we would need to synch. to ensure some other thread + * did not create a smaller one) + */ + private static byte[] SKIP_BYTE_BUFFER; + + // read toByteArray + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copy bytes from an InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + return copyLarge(input, output, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param buffer the buffer to use for the copy + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, byte[] buffer) + throws IOException { + long count = 0; + int n = 0; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, long inputOffset, long length) + throws IOException { + return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Skip bytes from an input byte stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * subclasses of {@link java.io.Reader}. + * + * @param input byte stream to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * + * @see InputStream#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @since 2.0 + */ + public static long skip(InputStream input, long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data + * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer + * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) + */ + if (SKIP_BYTE_BUFFER == null) { + SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE]; + } + long remain = toSkip; + while (remain > 0) { + long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Copy some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @param buffer the buffer to use for the copy + * + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, + final long inputOffset, final long length, byte[] buffer) throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + final int bufferLength = buffer.length; + int bytesToRead = bufferLength; + if (length > 0 && length < bufferLength) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, bufferLength); + } + } + return totalRead; + } + + /** + * Skip the requested number of bytes or fail if there are not enough left. + *

+ * This allows for the possibility that {@link InputStream#skip(long)} may + * not skip as many bytes as requested (most likely because of reaching EOF). + * + * @param input stream to skip + * @param toSkip the number of bytes to skip + * @see InputStream#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @since 2.0 + */ + public static void skipFully(InputStream input, long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Unconditionally close an InputStream. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an OutputStream. + *

+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(OutputStream output) { + try { + if (output != null) { + output.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Closes a Closeable unconditionally. + *

+ * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+   *   Closeable closeable = null;
+   *   try {
+   *       closeable = new FileReader("foo.txt");
+   *       // process closeable
+   *       closeable.close();
+   *   } catch (Exception e) {
+   *       // error handling
+   *   } finally {
+   *       IOUtils.closeQuietly(closeable);
+   *   }
+   * 
+ * + * @param closeable the object to close, may be null or already closed + * @since 2.0 + */ + public static void closeQuietly(final Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (final IOException ioe) { + // ignore + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseImpreciseDateFormat.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseImpreciseDateFormat.java new file mode 100644 index 0000000..d8580ad --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseImpreciseDateFormat.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.SimpleTimeZone; + +/** + * Used to parse legacy created_at and updated_at from disk. It is only precise to the second. + */ +/** package */ class ParseImpreciseDateFormat { + private static final String TAG = "ParseDateFormat"; + + private static final ParseImpreciseDateFormat INSTANCE = new ParseImpreciseDateFormat(); + public static ParseImpreciseDateFormat getInstance() { + return INSTANCE; + } + + // SimpleDateFormat isn't inherently thread-safe + private final Object lock = new Object(); + + private final DateFormat dateFormat; + + private ParseImpreciseDateFormat() { + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + format.setTimeZone(new SimpleTimeZone(0, "GMT")); + dateFormat = format; + } + + /* package */ Date parse(String dateString) { + synchronized (lock) { + try { + return dateFormat.parse(dateString); + } catch (java.text.ParseException e) { + // Should never happen + PLog.e(TAG, "could not parse date: " + dateString, e); + return null; + } + } + } + + /* package */ String format(Date date) { + synchronized (lock) { + return dateFormat.format(date); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIncrementOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIncrementOperation.java new file mode 100644 index 0000000..b2c56c2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseIncrementOperation.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An operation that increases a numeric field's value by a given amount. + */ +/** package */ class ParseIncrementOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Increment"; + + private final Number amount; + + public ParseIncrementOperation(Number amount) { + this.amount = amount; + } + + @Override + public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { + JSONObject output = new JSONObject(); + output.put("__op", OP_NAME); + output.put("amount", amount); + return output; + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number + } + + @Override + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + if (previous == null) { + return this; + } else if (previous instanceof ParseDeleteOperation) { + return new ParseSetOperation(amount); + } else if (previous instanceof ParseSetOperation) { + Object oldValue = ((ParseSetOperation) previous).getValue(); + if (oldValue instanceof Number) { + return new ParseSetOperation(Numbers.add((Number) oldValue, amount)); + } else { + throw new IllegalArgumentException("You cannot increment a non-number."); + } + } else if (previous instanceof ParseIncrementOperation) { + Number oldAmount = ((ParseIncrementOperation) previous).amount; + return new ParseIncrementOperation(Numbers.add(oldAmount, amount)); + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } + + @Override + public Object apply(Object oldValue, String key) { + if (oldValue == null) { + return amount; + } else if (oldValue instanceof Number) { + return Numbers.add((Number) oldValue, amount); + } else { + throw new IllegalArgumentException("You cannot increment a non-number."); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseInstallation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseInstallation.java new file mode 100644 index 0000000..5ad0be0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseInstallation.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import bolts.Continuation; +import bolts.Task; + +/** + * The {@code ParseInstallation} is a local representation of installation data that can be saved + * and retrieved from the Parse cloud. + */ +@ParseClassName("_Installation") +public class ParseInstallation extends ParseObject { + private static final String TAG = "com.parse.ParseInstallation"; + + private static final String KEY_OBJECT_ID = "objectId"; + private static final String KEY_INSTALLATION_ID = "installationId"; + private static final String KEY_DEVICE_TYPE = "deviceType"; + private static final String KEY_APP_NAME = "appName"; + private static final String KEY_APP_IDENTIFIER = "appIdentifier"; + private static final String KEY_PARSE_VERSION = "parseVersion"; + private static final String KEY_DEVICE_TOKEN = "deviceToken"; + private static final String KEY_PUSH_TYPE = "pushType"; + private static final String KEY_TIME_ZONE = "timeZone"; + private static final String KEY_LOCALE = "localeIdentifier"; + private static final String KEY_APP_VERSION = "appVersion"; + /* package */ static final String KEY_CHANNELS = "channels"; + + private static final List READ_ONLY_FIELDS = Collections.unmodifiableList( + Arrays.asList(KEY_DEVICE_TYPE, KEY_INSTALLATION_ID, KEY_DEVICE_TOKEN, KEY_PUSH_TYPE, + KEY_TIME_ZONE, KEY_LOCALE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION, + KEY_APP_IDENTIFIER, KEY_OBJECT_ID)); + + // TODO(mengyan): Inject into ParseInstallationInstanceController + /* package */ static ParseCurrentInstallationController getCurrentInstallationController() { + return ParseCorePlugins.getInstance().getCurrentInstallationController(); + } + + public static ParseInstallation getCurrentInstallation() { + try { + return ParseTaskUtils.wait( + getCurrentInstallationController().getAsync()); + } catch (ParseException e) { + // In order to have backward compatibility, we swallow the exception silently. + return null; + } + } + + /** + * Constructs a query for {@code ParseInstallation}. + *

+ * Note: We only allow the following types of queries for installations: + *

+   * query.get(objectId)
+   * query.whereEqualTo("installationId", value)
+   * query.whereMatchesKeyInQuery("installationId", keyInQuery, query)
+   * 
+ *

+ * You can add additional query clauses, but one of the above must appear as a top-level + * {@code AND} clause in the query. + * + * @see com.parse.ParseQuery#getQuery(Class) + */ + public static ParseQuery getQuery() { + return ParseQuery.getQuery(ParseInstallation.class); + } + + public ParseInstallation() { + // do nothing + } + + /** + * Returns the unique ID of this installation. + * + * @return A UUID that represents this device. + */ + public String getInstallationId() { + return getString(KEY_INSTALLATION_ID); + } + + @Override + public void setObjectId(String newObjectId) { + throw new RuntimeException("Installation's objectId cannot be changed"); + } + + @Override + /* package */ boolean needsDefaultACL() { + return false; + } + + @Override + /* package */ boolean isKeyMutable(String key) { + return !READ_ONLY_FIELDS.contains(key); + } + + @Override + /* package */ void updateBeforeSave() { + super.updateBeforeSave(); + if (getCurrentInstallationController().isCurrent(ParseInstallation.this)) { + updateTimezone(); + updateVersionInfo(); + updateDeviceInfo(); + updateLocaleIdentifier(); + } + } + + @Override + /* package */ Task fetchAsync( + final String sessionToken, final Task toAwait) { + synchronized (mutex) { + // Because the Service and the global currentInstallation are different objects, we may not + // have the same ObjectID (we never will at bootstrap). The server has a special hack for + // _Installation where save with an existing InstallationID will merge Object IDs + Task result; + if (getObjectId() == null) { + result = saveAsync(sessionToken, toAwait); + } else { + result = Task.forResult(null); + } + return result.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return ParseInstallation.super.fetchAsync(sessionToken, toAwait); + } + }); + } + } + + @Override + /* package */ Task saveAsync(final String sessionToken, final Task toAwait) { + return super.saveAsync(sessionToken, toAwait).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Retry the fetch as a save operation because this Installation was deleted on the server. + if (task.getError() != null + && task.getError() instanceof ParseException) { + int errCode = ((ParseException) task.getError()).getCode(); + if (errCode == ParseException.OBJECT_NOT_FOUND + || (errCode == ParseException.MISSING_REQUIRED_FIELD_ERROR && getObjectId() == null)) { + synchronized (mutex) { + setState(new State.Builder(getState()).objectId(null).build()); + markAllFieldsDirty(); + return ParseInstallation.super.saveAsync(sessionToken, toAwait); + } + } + } + return task; + } + }); + } + + @Override + /* package */ Task handleSaveResultAsync(ParseObject.State result, + ParseOperationSet operationsBeforeSave) { + Task task = super.handleSaveResultAsync(result, operationsBeforeSave); + + if (result == null) { // Failure + return task; + } + + return task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getCurrentInstallationController().setAsync(ParseInstallation.this); + } + }); + } + + @Override + /* package */ Task handleFetchResultAsync(final ParseObject.State newState) { + return super.handleFetchResultAsync(newState).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getCurrentInstallationController().setAsync(ParseInstallation.this); + } + }); + } + + // Android documentation states that getID may return one of many forms: America/LosAngeles, + // GMT-, or code. We only accept the first on the server, so for now we will not upload + // time zones from devices reporting other formats. + private void updateTimezone() { + String zone = TimeZone.getDefault().getID(); + if ((zone.indexOf('/') > 0 || zone.equals("GMT")) && !zone.equals(get(KEY_TIME_ZONE))) { + performPut(KEY_TIME_ZONE, zone); + } + } + + private void updateVersionInfo() { + synchronized (mutex) { + try { + Context context = Parse.getApplicationContext(); + String packageName = context.getPackageName(); + PackageManager pm = context.getPackageManager(); + PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); + String appVersion = pkgInfo.versionName; + String appName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString(); + + if (packageName != null && !packageName.equals(get(KEY_APP_IDENTIFIER))) { + performPut(KEY_APP_IDENTIFIER, packageName); + } + if (appName != null && !appName.equals(get(KEY_APP_NAME))) { + performPut(KEY_APP_NAME, appName); + } + if (appVersion != null && !appVersion.equals(get(KEY_APP_VERSION))) { + performPut(KEY_APP_VERSION, appVersion); + } + } catch (PackageManager.NameNotFoundException e) { + PLog.w(TAG, "Cannot load package info; will not be saved to installation"); + } + + if (!VERSION_NAME.equals(get(KEY_PARSE_VERSION))) { + performPut(KEY_PARSE_VERSION, VERSION_NAME); + } + } + } + + /* + * Save locale in the following format: + * [language code]-[country code] + * + * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + * The country codes are two-letter uppercase ISO country codes (such as "US") as defined by + * ISO 3166-1. + * + * Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language + * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This + * rewriting happens even if you construct your own {@code Locale} object, not just for + * instances returned by the various lookup methods. + */ + private void updateLocaleIdentifier() { + final Locale locale = Locale.getDefault(); + + String language = locale.getLanguage(); + String country = locale.getCountry(); + + if (TextUtils.isEmpty(language)) { + return; + } + + // rewrite depreciated two-letter codes + if (language.equals("iw")) language = "he"; // Hebrew + if (language.equals("in")) language = "id"; // Indonesian + if (language.equals("ji")) language = "yi"; // Yiddish + + String localeString = language; + + if (!TextUtils.isEmpty(country)) { + localeString = String.format(Locale.US, "%s-%s", language, country); + } + + if (!localeString.equals(get(KEY_LOCALE))) { + performPut(KEY_LOCALE, localeString); + } + } + + // TODO(mengyan): Move to ParseInstallationInstanceController + /* package */ void updateDeviceInfo() { + updateDeviceInfo(ParsePlugins.get().installationId()); + } + + /* package */ void updateDeviceInfo(InstallationId installationId) { + /* + * If we don't have an installationId, use the one that comes from the installationId file on + * disk. This should be impossible since we set the installationId in setDefaultValues. + */ + if (!has(KEY_INSTALLATION_ID)) { + performPut(KEY_INSTALLATION_ID, installationId.get()); + } + String deviceType = "android"; + if (!deviceType.equals(get(KEY_DEVICE_TYPE))) { + performPut(KEY_DEVICE_TYPE, deviceType); + } + } + + /* package */ PushType getPushType() { + return PushType.fromString(super.getString(KEY_PUSH_TYPE)); + } + + /* package */ void setPushType(PushType pushType) { + if (pushType != null) { + performPut(KEY_PUSH_TYPE, pushType.toString()); + } + } + + /* package */ void removePushType() { + performRemove(KEY_PUSH_TYPE); + } + + /* package */ String getDeviceToken() { + return super.getString(KEY_DEVICE_TOKEN); + } + + /* package */ void setDeviceToken(String deviceToken) { + if (deviceToken != null && deviceToken.length() > 0) { + performPut(KEY_DEVICE_TOKEN, deviceToken); + } + } + + /* package */ void removeDeviceToken() { + performRemove(KEY_DEVICE_TOKEN); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseJSONUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseJSONUtils.java new file mode 100644 index 0000000..8748a63 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseJSONUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Static utility methods pertaining to {@link JSONObject} and {@link JSONArray} instances. + */ +/** package */ class ParseJSONUtils { + + /** + * Creates a copy of {@code copyFrom}, excluding the keys from {@code excludes}. + */ + public static JSONObject create(JSONObject copyFrom, Collection excludes) { + JSONObject json = new JSONObject(); + Iterator iterator = copyFrom.keys(); + while (iterator.hasNext()) { + String name = iterator.next(); + if (excludes.contains(name)) { + continue; + } + try { + json.put(name, copyFrom.opt(name)); + } catch (JSONException e) { + // This shouldn't ever happen since it'll only throw if `name` is null + throw new RuntimeException(e); + } + } + return json; + } + + /** + * A helper for nonugly iterating over JSONObject keys. + */ + public static Iterable keys(JSONObject object) { + final JSONObject finalObject = object; + return new Iterable() { + @Override + public Iterator iterator() { + return finalObject.keys(); + } + }; + } + + /** + * A helper for returning the value mapped by a list of keys, ordered by priority. + */ + public static int getInt(JSONObject object, List keys) throws JSONException { + for (String key : keys) { + try { + return object.getInt(key); + } catch (JSONException e) { + // do nothing + } + } + throw new JSONException("No value for " + keys); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseKeyValueCache.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseKeyValueCache.java new file mode 100644 index 0000000..3441ad9 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseKeyValueCache.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; + +/** + * Used for ParseQuery caching. + */ +/** package */ class ParseKeyValueCache { + + private static final String TAG = "ParseKeyValueCache"; + private static final String DIR_NAME = "ParseKeyValueCache"; + + // We limit the cache to 2MB because that's about what the default browser + // uses. + /* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_BYTES = 2 * 1024 * 1024; + // We limit to 1000 cache files to avoid taking too long while scanning the + // cache + /* package */ static final int DEFAULT_MAX_KEY_VALUE_CACHE_FILES = 1000; + + /** + * Prevent multiple threads from modifying the cache at the same time. + */ + private static final Object MUTEX_IO = new Object(); + + /* package */ static int maxKeyValueCacheBytes = DEFAULT_MAX_KEY_VALUE_CACHE_BYTES; + /* package */ static int maxKeyValueCacheFiles = DEFAULT_MAX_KEY_VALUE_CACHE_FILES; + + private static File directory; + + // Creates a directory to keep cache-type files in. + // The operating system will automatically clear out these files first + // when space gets low. + /* package */ static void initialize(Context context) { + initialize(new File(context.getCacheDir(), DIR_NAME)); + } + + /* package for tests */ static void initialize(File path) { + if (!path.isDirectory() && !path.mkdir()) { + throw new RuntimeException("Could not create ParseKeyValueCache directory"); + } + directory = path; + } + + private static File getKeyValueCacheDir() { + if (directory != null && !directory.exists()) { + directory.mkdir(); + } + return directory; + } + + /** + * How many files are in the key-value cache. + */ + /* package */ static int size() { + File[] files = getKeyValueCacheDir().listFiles(); + if (files == null) { + return 0; + } + return files.length; + } + + private static File getKeyValueCacheFile(String key) { + final String suffix = '.' + key; + File[] matches = getKeyValueCacheDir().listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.endsWith(suffix); + } + }); + return (matches == null || matches.length == 0) ? null : matches[0]; + } + + // Badly formatted files return the epoch + private static long getKeyValueCacheAge(File cacheFile) { + // Format: . + String name = cacheFile.getName(); + try { + return Long.parseLong(name.substring(0, name.indexOf('.'))); + } catch (NumberFormatException e) { + return 0; + } + } + + private static File createKeyValueCacheFile(String key) { + String filename = String.valueOf(new Date().getTime()) + '.' + key; + return new File(getKeyValueCacheDir(), filename); + } + + // Removes all the cache entries. + /* package */ static void clearKeyValueCacheDir() { + synchronized (MUTEX_IO) { + File dir = getKeyValueCacheDir(); + if (dir == null) { + return; + } + File[] entries = dir.listFiles(); + if (entries == null) { + return; + } + for (File entry : entries) { + entry.delete(); + } + } + } + + // Saves a key-value pair to the cache + /* package */ static void saveToKeyValueCache(String key, String value) { + synchronized (MUTEX_IO) { + File prior = getKeyValueCacheFile(key); + if (prior != null) { + prior.delete(); + } + File f = createKeyValueCacheFile(key); + try { + ParseFileUtils.writeByteArrayToFile(f, value.getBytes("UTF-8")); + } catch (IOException e) { + // do nothing + } + + // Check if we should kick out old cache entries + File[] files = getKeyValueCacheDir().listFiles(); + // We still need this check since dir.mkdir() may fail + if (files == null || files.length == 0) { + return; + } + + int numFiles = files.length; + int numBytes = 0; + for (File file : files) { + numBytes += file.length(); + } + + // If we do not need to clear the cache, simply return + if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) { + return; + } + + // We need to kick out some cache entries. + // Sort oldest-first. We touch on read so mtime is really LRU. + // Sometimes (i.e. tests) the time of lastModified isn't granular enough, + // so we resort + // to sorting by the file name which is always prepended with time in ms + Arrays.sort(files, new Comparator() { + @Override + public int compare(File f1, File f2) { + int dateCompare = Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); + if (dateCompare != 0) { + return dateCompare; + } else { + return f1.getName().compareTo(f2.getName()); + } + } + }); + + for (File file : files) { + numFiles--; + numBytes -= file.length(); + file.delete(); + + if (numFiles <= maxKeyValueCacheFiles && numBytes <= maxKeyValueCacheBytes) { + break; + } + } + } + } + + // Clears a key from the cache if it's there. If it's not there, this is a + // no-op. + /* package */ static void clearFromKeyValueCache(String key) { + synchronized (MUTEX_IO) { + File file = getKeyValueCacheFile(key); + if (file != null) { + file.delete(); + } + } + } + + // Loads a value from the key-value cache. + // Returns null if nothing is there. + /* package */ static String loadFromKeyValueCache(final String key, final long maxAgeMilliseconds) { + synchronized (MUTEX_IO) { + File file = getKeyValueCacheFile(key); + if (file == null) { + return null; + } + + Date now = new Date(); + long oldestAcceptableAge = Math.max(0, now.getTime() - maxAgeMilliseconds); + if (getKeyValueCacheAge(file) < oldestAcceptableAge) { + return null; + } + + // Update mtime to make the LRU work + file.setLastModified(now.getTime()); + + try { + RandomAccessFile f = new RandomAccessFile(file, "r"); + byte[] bytes = new byte[(int) f.length()]; + f.readFully(bytes); + f.close(); + return new String(bytes, "UTF-8"); + } catch (IOException e) { + PLog.e(TAG, "error reading from cache", e); + return null; + } + } + } + + // Returns null if the value does not exist or is not json + /* package */ static JSONObject jsonFromKeyValueCache(String key, long maxAgeMilliseconds) { + String raw = loadFromKeyValueCache(key, maxAgeMilliseconds); + if (raw == null) { + return null; + } + + try { + return new JSONObject(raw); + } catch (JSONException e) { + PLog.e(TAG, "corrupted cache for " + key, e); + clearFromKeyValueCache(key); + return null; + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseMulticastDelegate.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseMulticastDelegate.java new file mode 100644 index 0000000..a2a48ac --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseMulticastDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** package */ class ParseMulticastDelegate { + private final List> callbacks; + + public ParseMulticastDelegate() { + callbacks = new LinkedList<>(); + } + + public void subscribe(ParseCallback2 callback) { + callbacks.add(callback); + } + + public void unsubscribe(ParseCallback2 callback) { + callbacks.remove(callback); + } + + public void invoke(T result, ParseException exception) { + for (ParseCallback2 callback : new ArrayList<>(callbacks)) { + callback.done(result, exception); + } + } + + public void clear() { + callbacks.clear(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseNotificationManager.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseNotificationManager.java new file mode 100644 index 0000000..62cce39 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseNotificationManager.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.concurrent.atomic.AtomicInteger; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.util.SparseIntArray; + +/** + * A utility class for building and showing notifications. + */ +/** package */ class ParseNotificationManager { + public static final String TAG = "com.parse.ParseNotificationManager"; + + public static class Singleton { + private static final ParseNotificationManager INSTANCE = new ParseNotificationManager(); + } + + public static ParseNotificationManager getInstance() { + return Singleton.INSTANCE; + } + + private final AtomicInteger notificationCount = new AtomicInteger(0); + private volatile boolean shouldShowNotifications = true; + + public void setShouldShowNotifications(boolean show) { + shouldShowNotifications = show; + } + + public int getNotificationCount() { + return notificationCount.get(); + } + + public void showNotification(Context context, Notification notification) { + if (context != null && notification != null) { + notificationCount.incrementAndGet(); + + if (shouldShowNotifications) { + // Fire off the notification + NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // Pick an id that probably won't overlap anything + int notificationId = (int)System.currentTimeMillis(); + + try { + nm.notify(notificationId, notification); + } catch (SecurityException e) { + // Some phones throw an exception for unapproved vibration + notification.defaults = Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND; + nm.notify(notificationId, notification); + } + } + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObject.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObject.java new file mode 100644 index 0000000..903c210 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObject.java @@ -0,0 +1,4386 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; + +import bolts.Capture; +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * The {@code ParseObject} is a local representation of data that can be saved and retrieved from + * the Parse cloud. + *

+ * The basic workflow for creating new data is to construct a new {@code ParseObject}, use + * {@link #put(String, Object)} to fill it with data, and then use {@link #saveInBackground()} to + * persist to the cloud. + *

+ * The basic workflow for accessing existing data is to use a {@link ParseQuery} to specify which + * existing data to retrieve. + */ +public class ParseObject implements Parcelable { + private static final String AUTO_CLASS_NAME = "_Automatic"; + /* package */ static final String VERSION_NAME = BuildConfig.VERSION_NAME; + private static final String TAG = "ParseObject"; + + /* + REST JSON Keys + */ + private static final String KEY_OBJECT_ID = "objectId"; + private static final String KEY_CLASS_NAME = "className"; + private static final String KEY_ACL = "ACL"; + private static final String KEY_CREATED_AT = "createdAt"; + private static final String KEY_UPDATED_AT = "updatedAt"; + + /* + Internal JSON Keys - Used to store internal data when persisting {@code ParseObject}s locally. + */ + private static final String KEY_COMPLETE = "__complete"; + private static final String KEY_OPERATIONS = "__operations"; + // Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s + // correctly, and helps constructing the {@code State.availableKeys()} set. + private static final String KEY_SELECTED_KEYS = "__selectedKeys"; + /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; + // Because Grantland messed up naming this... We'll only try to read from this for backward + // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete + // and not check after a while + private static final String KEY_IS_DELETING_EVENTUALLY_OLD = "isDeletingEventually"; + + private static ParseObjectController getObjectController() { + return ParseCorePlugins.getInstance().getObjectController(); + } + + private static LocalIdManager getLocalIdManager() { + return ParseCorePlugins.getInstance().getLocalIdManager(); + } + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + /** package */ static class State { + + public static Init newBuilder(String className) { + if ("_User".equals(className)) { + return new ParseUser.State.Builder(); + } + return new Builder(className); + } + + /* package */ static State createFromParcel(Parcel source, ParseParcelDecoder decoder) { + String className = source.readString(); + if ("_User".equals(className)) { + return new ParseUser.State(source, className, decoder); + } + return new State(source, className, decoder); + } + + /** package */ static abstract class Init { + + private final String className; + private String objectId; + private long createdAt = -1; + private long updatedAt = -1; + private boolean isComplete; + private Set availableKeys = new HashSet<>(); + /* package */ Map serverData = new HashMap<>(); + + public Init(String className) { + this.className = className; + } + + /* package */ Init(State state) { + className = state.className(); + objectId = state.objectId(); + createdAt = state.createdAt(); + updatedAt = state.updatedAt(); + availableKeys = state.availableKeys(); + for (String key : state.keySet()) { + serverData.put(key, state.get(key)); + availableKeys.add(key); + } + isComplete = state.isComplete(); + } + + /* package */ abstract T self(); + + /* package */ abstract S build(); + + public T objectId(String objectId) { + this.objectId = objectId; + return self(); + } + + public T createdAt(Date createdAt) { + this.createdAt = createdAt.getTime(); + return self(); + } + + public T createdAt(long createdAt) { + this.createdAt = createdAt; + return self(); + } + + public T updatedAt(Date updatedAt) { + this.updatedAt = updatedAt.getTime(); + return self(); + } + + public T updatedAt(long updatedAt) { + this.updatedAt = updatedAt; + return self(); + } + + public T isComplete(boolean complete) { + isComplete = complete; + return self(); + } + + public T put(String key, Object value) { + serverData.put(key, value); + availableKeys.add(key); + return self(); + } + + public T remove(String key) { + serverData.remove(key); + return self(); + } + + public T availableKeys(Collection keys) { + for (String key : keys) { + availableKeys.add(key); + } + return self(); + } + + public T clear() { + objectId = null; + createdAt = -1; + updatedAt = -1; + isComplete = false; + serverData.clear(); + availableKeys.clear(); + return self(); + } + + /** + * Applies a {@code State} on top of this {@code Builder} instance. + * + * @param other The {@code State} to apply over this instance. + * @return A new {@code Builder} instance. + */ + public T apply(State other) { + if (other.objectId() != null) { + objectId(other.objectId()); + } + if (other.createdAt() > 0) { + createdAt(other.createdAt()); + } + if (other.updatedAt() > 0) { + updatedAt(other.updatedAt()); + } + isComplete(isComplete || other.isComplete()); + for (String key : other.keySet()) { + put(key, other.get(key)); + } + availableKeys(other.availableKeys()); + return self(); + } + + public T apply(ParseOperationSet operations) { + for (String key : operations.keySet()) { + ParseFieldOperation operation = operations.get(key); + Object oldValue = serverData.get(key); + Object newValue = operation.apply(oldValue, key); + if (newValue != null) { + put(key, newValue); + } else { + remove(key); + } + } + return self(); + } + } + + /* package */ static class Builder extends Init { + + public Builder(String className) { + super(className); + } + + public Builder(State state) { + super(state); + } + + @Override + /* package */ Builder self() { + return this; + } + + public State build() { + return new State(this); + } + } + + private final String className; + private final String objectId; + private final long createdAt; + private final long updatedAt; + private final Map serverData; + private final Set availableKeys; + private final boolean isComplete; + + /* package */ State(Init builder) { + className = builder.className; + objectId = builder.objectId; + createdAt = builder.createdAt; + updatedAt = builder.updatedAt > 0 + ? builder.updatedAt + : createdAt; + serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData)); + isComplete = builder.isComplete; + availableKeys = new HashSet<>(builder.availableKeys); + } + + /* package */ State(Parcel parcel, String clazz, ParseParcelDecoder decoder) { + className = clazz; // Already read + objectId = parcel.readByte() == 1 ? parcel.readString() : null; + createdAt = parcel.readLong(); + long updated = parcel.readLong(); + updatedAt = updated > 0 ? updated : createdAt; + int size = parcel.readInt(); + HashMap map = new HashMap<>(); + for (int i = 0; i < size; i++) { + String key = parcel.readString(); + Object obj = decoder.decode(parcel); + map.put(key, obj); + } + serverData = Collections.unmodifiableMap(map); + isComplete = parcel.readByte() == 1; + List available = new ArrayList<>(); + parcel.readStringList(available); + availableKeys = new HashSet<>(available); + } + + @SuppressWarnings("unchecked") + public > T newBuilder() { + return (T) new Builder(this); + } + + public String className() { + return className; + } + + public String objectId() { + return objectId; + } + + public long createdAt() { + return createdAt; + } + + public long updatedAt() { + return updatedAt; + } + + public boolean isComplete() { + return isComplete; + } + + public Object get(String key) { + return serverData.get(key); + } + + public Set keySet() { + return serverData.keySet(); + } + + // Available keys for this object. With respect to keySet(), this includes also keys that are + // undefined in the server, but that should be accessed without throwing. + // These extra keys come e.g. from ParseQuery.selectKeys(). Selected keys must be available to + // get() methods even if undefined, for consistency with complete objects. + // For a complete object, this set is equal to keySet(). + public Set availableKeys() { + return availableKeys; + } + + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeString(className); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) { + dest.writeString(objectId); + } + dest.writeLong(createdAt); + dest.writeLong(updatedAt); + dest.writeInt(serverData.size()); + Set keys = serverData.keySet(); + for (String key : keys) { + dest.writeString(key); + encoder.encode(serverData.get(key), dest); + } + dest.writeByte(isComplete ? (byte) 1 : 0); + dest.writeStringList(new ArrayList<>(availableKeys)); + } + + @Override + public String toString() { + return String.format(Locale.US, "%s@%s[" + + "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + + "serverData=%s, availableKeys=%s]", + getClass().getName(), + Integer.toHexString(hashCode()), + className, + objectId, + createdAt, + updatedAt, + isComplete, + serverData, + availableKeys); + } + } + + /* package */ final Object mutex = new Object(); + /* package */ final TaskQueue taskQueue = new TaskQueue(); + + private State state; + /* package */ final LinkedList operationSetQueue; + + // Cached State + private final Map estimatedData; + + /* package */ String localId; + private final ParseMulticastDelegate saveEvent = new ParseMulticastDelegate<>(); + + /* package */ boolean isDeleted; + /* package */ boolean isDeleting; // Since delete ops are queued, we don't need a counter. + //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. + /* package */ int isDeletingEventually; + private boolean ldsEnabledWhenParceling; + + private static final ThreadLocal isCreatingPointerForObjectId = + new ThreadLocal() { + @Override + protected String initialValue() { + return null; + } + }; + + /* + * This is used only so that we can pass it to createWithoutData as the objectId to make it create + * an unfetched pointer that has no objectId. This is useful only in the context of the offline + * store, where you can have an unfetched pointer for an object that can later be fetched from the + * store. + */ + /* package */ private static final String NEW_OFFLINE_OBJECT_ID_PLACEHOLDER = + "*** Offline Object ***"; + + /** + * The base class constructor to call in subclasses. Uses the class name specified with the + * {@link ParseClassName} annotation on the subclass. + */ + protected ParseObject() { + this(AUTO_CLASS_NAME); + } + + /** + * Constructs a new {@code ParseObject} with no data in it. A {@code ParseObject} constructed in + * this way will not have an objectId and will not persist to the database until {@link #save()} + * is called. + *

+ * Class names must be alphanumerical plus underscore, and start with a letter. It is recommended + * to name classes in PascalCaseLikeThis. + * + * @param theClassName + * The className for this {@code ParseObject}. + */ + public ParseObject(String theClassName) { + // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the + // right thing with subclasses. It's ugly and terrible, but it does provide the development + // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the + // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? + String objectIdForPointer = isCreatingPointerForObjectId.get(); + + if (theClassName == null) { + throw new IllegalArgumentException( + "You must specify a Parse class name when creating a new ParseObject."); + } + if (AUTO_CLASS_NAME.equals(theClassName)) { + theClassName = getSubclassingController().getClassName(getClass()); + } + + // If this is supposed to be created by a factory but wasn't, throw an exception. + if (!getSubclassingController().isSubclassValid(theClassName, getClass())) { + throw new IllegalArgumentException( + "You must create this type of ParseObject using ParseObject.create() or the proper subclass."); + } + + operationSetQueue = new LinkedList<>(); + operationSetQueue.add(new ParseOperationSet()); + estimatedData = new HashMap<>(); + + State.Init builder = newStateBuilder(theClassName); + // When called from new, assume hasData for the whole object is true. + if (objectIdForPointer == null) { + setDefaultValues(); + builder.isComplete(true); + } else { + if (!objectIdForPointer.equals(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER)) { + builder.objectId(objectIdForPointer); + } + builder.isComplete(false); + } + // This is a new untouched object, we don't need cache rebuilding, etc. + state = builder.build(); + + OfflineStore store = Parse.getLocalDatastore(); + if (store != null) { + store.registerNewObject(this); + } + } + + /** + * Creates a new {@code ParseObject} based upon a class name. If the class name is a special type + * (e.g. for {@code ParseUser}), then the appropriate type of {@code ParseObject} is returned. + * + * @param className + * The class of object to create. + * @return A new {@code ParseObject} for the given class name. + */ + public static ParseObject create(String className) { + return getSubclassingController().newInstance(className); + } + + /** + * Creates a new {@code ParseObject} based upon a subclass type. Note that the object will be + * created based upon the {@link ParseClassName} of the given subclass type. For example, calling + * create(ParseUser.class) may create an instance of a custom subclass of {@code ParseUser}. + * + * @param subclass + * The class of object to create. + * @return A new {@code ParseObject} based upon the class name of the given subclass type. + */ + @SuppressWarnings("unchecked") + public static T create(Class subclass) { + return (T) create(getSubclassingController().getClassName(subclass)); + } + + /** + * Creates a reference to an existing {@code ParseObject} for use in creating associations between + * {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return + * {@code false} until {@link #fetchIfNeeded()} or {@link #refresh()} has been called. No network + * request will be made. + * + * @param className + * The object's class. + * @param objectId + * The object id for the referenced object. + * @return A {@code ParseObject} without data. + */ + public static ParseObject createWithoutData(String className, String objectId) { + OfflineStore store = Parse.getLocalDatastore(); + try { + if (objectId == null) { + isCreatingPointerForObjectId.set(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER); + } else { + isCreatingPointerForObjectId.set(objectId); + } + ParseObject object = null; + if (store != null && objectId != null) { + object = store.getObject(className, objectId); + } + + if (object == null) { + object = create(className); + if (object.hasChanges()) { + throw new IllegalStateException( + "A ParseObject subclass default constructor must not make changes " + + "to the object that cause it to be dirty." + ); + } + } + + return object; + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Failed to create instance of subclass.", e); + } finally { + isCreatingPointerForObjectId.set(null); + } + } + + /** + * Creates a reference to an existing {@code ParseObject} for use in creating associations between + * {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return + * {@code false} until {@link #fetchIfNeeded()} or {@link #refresh()} has been called. No network + * request will be made. + * + * @param subclass + * The {@code ParseObject} subclass to create. + * @param objectId + * The object id for the referenced object. + * @return A {@code ParseObject} without data. + */ + @SuppressWarnings({"unused", "unchecked"}) + public static T createWithoutData(Class subclass, String objectId) { + return (T) createWithoutData(getSubclassingController().getClassName(subclass), objectId); + } + + /** + * Registers a custom subclass type with the Parse SDK, enabling strong-typing of those + * {@code ParseObject}s whenever they appear. Subclasses must specify the {@link ParseClassName} + * annotation and have a default constructor. + * + * @param subclass + * The subclass type to register. + */ + public static void registerSubclass(Class subclass) { + getSubclassingController().registerSubclass(subclass); + } + + /* package for tests */ static void unregisterSubclass(Class subclass) { + getSubclassingController().unregisterSubclass(subclass); + } + + /** + * Adds a task to the queue for all of the given objects. + */ + static Task enqueueForAll(final List objects, + Continuation> taskStart) { + // The task that will be complete when all of the child queues indicate they're ready to start. + final TaskCompletionSource readyToStart = new TaskCompletionSource<>(); + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + List locks = new ArrayList<>(objects.size()); + for (ParseObject obj : objects) { + locks.add(obj.taskQueue.getLock()); + } + LockSet lock = new LockSet(locks); + + lock.lock(); + try { + // The task produced by TaskStart + final Task fullTask; + try { + // By running this immediately, we allow everything prior to toAwait to run before waiting + // for all of the queues on all of the objects. + fullTask = taskStart.then(readyToStart.getTask()); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + + // Add fullTask to each of the objects' queues. + final List> childTasks = new ArrayList<>(); + for (ParseObject obj : objects) { + obj.taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + childTasks.add(task); + return fullTask; + } + }); + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + Task.whenAll(childTasks).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + readyToStart.setResult(null); + return null; + } + }); + return fullTask; + } finally { + lock.unlock(); + } + } + + /** + * Converts a {@code ParseObject.State} to a {@code ParseObject}. + * + * @param state + * The {@code ParseObject.State} to convert from. + * @return A {@code ParseObject} instance. + */ + /* package */ static T from(ParseObject.State state) { + @SuppressWarnings("unchecked") + T object = (T) ParseObject.createWithoutData(state.className(), state.objectId()); + synchronized (object.mutex) { + State newState; + if (state.isComplete()) { + newState = state; + } else { + newState = object.getState().newBuilder().apply(state).build(); + } + object.setState(newState); + } + return object; + } + + /** + * Creates a new {@code ParseObject} based on data from the Parse server. + * @param json + * The object's data. + * @param defaultClassName + * The className of the object, if none is in the JSON. + * @param decoder + * Delegate for knowing how to decode the values in the JSON. + * @param selectedKeys + * Set of keys selected when quering for this object. If none, the object is assumed to + * be complete, i.e. this is all the data for the object on the server. + */ + /* package */ static T fromJSON(JSONObject json, String defaultClassName, + ParseDecoder decoder, + Set selectedKeys) { + if (selectedKeys != null && !selectedKeys.isEmpty()) { + JSONArray keys = new JSONArray(selectedKeys); + try { + json.put(KEY_SELECTED_KEYS, keys); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return fromJSON(json, defaultClassName, decoder); + } + + /** + * Creates a new {@code ParseObject} based on data from the Parse server. + * @param json + * The object's data. It is assumed to be complete, unless the JSON has the + * {@link #KEY_SELECTED_KEYS} key. + * @param defaultClassName + * The className of the object, if none is in the JSON. + * @param decoder + * Delegate for knowing how to decode the values in the JSON. + */ + /* package */ static T fromJSON(JSONObject json, String defaultClassName, + ParseDecoder decoder) { + String className = json.optString(KEY_CLASS_NAME, defaultClassName); + if (className == null) { + return null; + } + String objectId = json.optString(KEY_OBJECT_ID, null); + boolean isComplete = !json.has(KEY_SELECTED_KEYS); + @SuppressWarnings("unchecked") + T object = (T) ParseObject.createWithoutData(className, objectId); + State newState = object.mergeFromServer(object.getState(), json, decoder, isComplete); + object.setState(newState); + return object; + } + + /** + * Method used by parse server webhooks implementation to convert raw JSON to Parse Object + * + * Method is used by parse server webhooks implementation to create a + * new {@code ParseObject} from the incoming json payload. The method is different from + * {@link #fromJSON(JSONObject, String, ParseDecoder, Set)} ()} in that it calls + * {@link #build(JSONObject, ParseDecoder)} which populates operation queue + * rather then the server data from the incoming JSON, as at external server the incoming + * JSON may not represent the actual server data. Also it handles + * {@link ParseFieldOperations} separately. + * + * @param json + * The object's data. + * @param decoder + * Delegate for knowing how to decode the values in the JSON. + */ + /* package */ static T fromJSONPayload( + JSONObject json, ParseDecoder decoder) { + String className = json.optString(KEY_CLASS_NAME); + if (className == null || ParseTextUtils.isEmpty(className)) { + return null; + } + String objectId = json.optString(KEY_OBJECT_ID, null); + @SuppressWarnings("unchecked") + T object = (T) ParseObject.createWithoutData(className, objectId); + object.build(json, decoder); + return object; + } + + //region Getter/Setter helper methods + + /* package */ State.Init newStateBuilder(String className) { + return new State.Builder(className); + } + + /* package */ State getState() { + synchronized (mutex) { + return state; + } + } + + /** + * Updates the current state of this object as well as updates our in memory cached state. + * + * @param newState The new state. + */ + /* package */ void setState(State newState) { + synchronized (mutex) { + setState(newState, true); + } + } + + private void setState(State newState, boolean notifyIfObjectIdChanges) { + synchronized (mutex) { + String oldObjectId = state.objectId(); + String newObjectId = newState.objectId(); + + state = newState; + + if (notifyIfObjectIdChanges && !ParseTextUtils.equals(oldObjectId, newObjectId)) { + notifyObjectIdChanged(oldObjectId, newObjectId); + } + + rebuildEstimatedData(); + } + } + + /** + * Accessor to the class name. + */ + public String getClassName() { + synchronized (mutex) { + return state.className(); + } + } + + /** + * This reports time as the server sees it, so that if you make changes to a {@code ParseObject}, then + * wait a while, and then call {@link #save()}, the updated time will be the time of the + * {@link #save()} call rather than the time the object was changed locally. + * + * @return The last time this object was updated on the server. + */ + public Date getUpdatedAt() { + long updatedAt = getState().updatedAt(); + return updatedAt > 0 + ? new Date(updatedAt) + : null; + } + + /** + * This reports time as the server sees it, so that if you create a {@code ParseObject}, then wait a + * while, and then call {@link #save()}, the creation time will be the time of the first + * {@link #save()} call rather than the time the object was created locally. + * + * @return The first time this object was saved on the server. + */ + public Date getCreatedAt() { + long createdAt = getState().createdAt(); + return createdAt > 0 + ? new Date(createdAt) + : null; + } + + //endregion + + /** + * Returns a set view of the keys contained in this object. This does not include createdAt, + * updatedAt, authData, or objectId. It does include things like username and ACL. + */ + public Set keySet() { + synchronized (mutex) { + return Collections.unmodifiableSet(estimatedData.keySet()); + } + } + + /** + * Copies all of the operations that have been performed on another object since its last save + * onto this one. + */ + /* package */ void copyChangesFrom(ParseObject other) { + synchronized (mutex) { + ParseOperationSet operations = other.operationSetQueue.getFirst(); + for (String key : operations.keySet()) { + performOperation(key, operations.get(key)); + } + } + } + + /* package */ void mergeFromObject(ParseObject other) { + synchronized (mutex) { + // If they point to the same instance, we don't need to merge. + if (this == other) { + return; + } + + State copy = other.getState().newBuilder().build(); + + // We don't want to notify if an objectId changed here since we utilize this method to merge + // an anonymous current user with a new ParseUser instance that's calling signUp(). This + // doesn't make any sense and we should probably remove that code in ParseUser. + // Otherwise, there shouldn't be any objectId changes here since this method is only otherwise + // used in fetchAll. + setState(copy, false); + } + } + + /** + * Clears changes to this object's {@code key} made since the last call to {@link #save()} or + * {@link #saveInBackground()}. + * + * @param key The {@code key} to revert changes for. + */ + public void revert(String key) { + synchronized (mutex) { + if (isDirty(key)) { + currentOperations().remove(key); + rebuildEstimatedData(); + } + } + } + + /** + * Clears any changes to this object made since the last call to {@link #save()} or + * {@link #saveInBackground()}. + */ + public void revert() { + synchronized (mutex) { + if (isDirty()) { + currentOperations().clear(); + rebuildEstimatedData(); + } + } + } + + /** + * Deep traversal on this object to grab a copy of any object referenced by this object. These + * instances may have already been fetched, and we don't want to lose their data when refreshing + * or saving. + * + * @return the map mapping from objectId to {@code ParseObject} which has been fetched. + */ + private Map collectFetchedObjects() { + final Map fetchedObjects = new HashMap<>(); + ParseTraverser traverser = new ParseTraverser() { + @Override + protected boolean visit(Object object) { + if (object instanceof ParseObject) { + ParseObject parseObj = (ParseObject) object; + State state = parseObj.getState(); + if (state.objectId() != null && state.isComplete()) { + fetchedObjects.put(state.objectId(), parseObj); + } + } + return true; + } + }; + traverser.traverse(estimatedData); + return fetchedObjects; + } + + /** + * Helper method called by {@link #fromJSONPayload(JSONObject, ParseDecoder)} + * + * The method helps webhooks implementation to build Parse object from raw JSON payload. + * It is different from {@link #mergeFromServer(State, JSONObject, ParseDecoder, boolean)} + * as the method saves the key value pairs (other than className, objectId, updatedAt and + * createdAt) in the operation queue rather than the server data. It also handles + * {@link ParseFieldOperations} differently. + * + * @param json : JSON object to be converted to Parse object + * @param decoder : Decoder to be used for Decoding JSON + */ + /* package */ void build(JSONObject json, ParseDecoder decoder) { + try { + State.Builder builder = new State.Builder(state) + .isComplete(true); + + builder.clear(); + + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + /* + __className: Used by fromJSONPayload, should be stripped out by the time it gets here... + */ + if (key.equals(KEY_CLASS_NAME)) { + continue; + } + if (key.equals(KEY_OBJECT_ID)) { + String newObjectId = json.getString(key); + builder.objectId(newObjectId); + continue; + } + if (key.equals(KEY_CREATED_AT)) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + if (key.equals(KEY_UPDATED_AT)) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + + Object value = json.get(key); + Object decodedObject = decoder.decode(value); + if (decodedObject instanceof ParseFieldOperation) { + performOperation(key, (ParseFieldOperation)decodedObject); + } + else { + put(key, decodedObject); + } + } + + setState(builder.build()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + + /** + * Merges from JSON in REST format. + * Updates this object with data from the server. + * + * @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder) + */ + /* package */ State mergeFromServer( + State state, JSONObject json, ParseDecoder decoder, boolean completeData) { + try { + // If server data is complete, consider this object to be fetched. + State.Init builder = state.newBuilder(); + if (completeData) { + builder.clear(); + } + builder.isComplete(state.isComplete() || completeData); + + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + /* + __type: Returned by queries and cloud functions to designate body is a ParseObject + __className: Used by fromJSON, should be stripped out by the time it gets here... + */ + if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) { + continue; + } + if (key.equals(KEY_OBJECT_ID)) { + String newObjectId = json.getString(key); + builder.objectId(newObjectId); + continue; + } + if (key.equals(KEY_CREATED_AT)) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + if (key.equals(KEY_UPDATED_AT)) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + if (key.equals(KEY_ACL)) { + ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder); + builder.put(KEY_ACL, acl); + continue; + } + if (key.equals(KEY_SELECTED_KEYS)) { + JSONArray safeKeys = json.getJSONArray(key); + if (safeKeys.length() > 0) { + Collection set = new HashSet<>(); + for (int i = 0; i < safeKeys.length(); i++) { + // Don't add nested keys. + String safeKey = safeKeys.getString(i); + if (safeKey.contains(".")) safeKey = safeKey.split("\\.")[0]; + set.add(safeKey); + } + builder.availableKeys(set); + } + continue; + } + + Object value = json.get(key); + if (value instanceof JSONObject && json.has(KEY_SELECTED_KEYS)) { + // This might be a ParseObject. Pass selected keys to understand if it is complete. + JSONArray selectedKeys = json.getJSONArray(KEY_SELECTED_KEYS); + JSONArray nestedKeys = new JSONArray(); + for (int i = 0; i < selectedKeys.length(); i++) { + String nestedKey = selectedKeys.getString(i); + if (nestedKey.startsWith(key + ".")) nestedKeys.put(nestedKey.substring(key.length() + 1)); + } + if (nestedKeys.length() > 0) { + ((JSONObject) value).put(KEY_SELECTED_KEYS, nestedKeys); + } + } + Object decodedObject = decoder.decode(value); + builder.put(key, decodedObject); + } + + return builder.build(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + //region LDS-processing methods. + + /** + * Convert to REST JSON for persisting in LDS. + * + * @see #mergeREST(State, org.json.JSONObject, ParseDecoder) + */ + /* package */ JSONObject toRest(ParseEncoder encoder) { + State state; + List operationSetQueueCopy; + synchronized (mutex) { + // mutex needed to lock access to state and operationSetQueue and operationSetQueue & children + // are mutable + state = getState(); + + // operationSetQueue is a List of Lists, so we'll need custom copying logic + int operationSetQueueSize = operationSetQueue.size(); + operationSetQueueCopy = new ArrayList<>(operationSetQueueSize); + for (int i = 0; i < operationSetQueueSize; i++) { + ParseOperationSet original = operationSetQueue.get(i); + ParseOperationSet copy = new ParseOperationSet(original); + operationSetQueueCopy.add(copy); + } + } + return toRest(state, operationSetQueueCopy, encoder); + } + + /* package */ JSONObject toRest( + State state, List operationSetQueue, ParseEncoder objectEncoder) { + // Public data goes in dataJSON; special fields go in objectJSON. + JSONObject json = new JSONObject(); + + try { + // REST JSON (State) + json.put(KEY_CLASS_NAME, state.className()); + if (state.objectId() != null) { + json.put(KEY_OBJECT_ID, state.objectId()); + } + if (state.createdAt() > 0) { + json.put(KEY_CREATED_AT, + ParseDateFormat.getInstance().format(new Date(state.createdAt()))); + } + if (state.updatedAt() > 0) { + json.put(KEY_UPDATED_AT, + ParseDateFormat.getInstance().format(new Date(state.updatedAt()))); + } + for (String key : state.keySet()) { + Object value = state.get(key); + json.put(key, objectEncoder.encode(value)); + } + + // Internal JSON + //TODO(klimt): We'll need to rip all this stuff out and put it somewhere else if we start + // using the REST api and want to send data to Parse. + json.put(KEY_COMPLETE, state.isComplete()); + json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); + JSONArray availableKeys = new JSONArray(state.availableKeys()); + json.put(KEY_SELECTED_KEYS, availableKeys); + + // Operation Set Queue + JSONArray operations = new JSONArray(); + for (ParseOperationSet operationSet : operationSetQueue) { + operations.put(operationSet.toRest(objectEncoder)); + } + json.put(KEY_OPERATIONS, operations); + + } catch (JSONException e) { + throw new RuntimeException("could not serialize object to JSON"); + } + + return json; + } + + /** + * Merge with REST JSON from LDS. + * + * @see #toRest(ParseEncoder) + */ + /* package */ void mergeREST(State state, JSONObject json, ParseDecoder decoder) { + ArrayList saveEventuallyOperationSets = new ArrayList<>(); + + synchronized (mutex) { + try { + boolean isComplete = json.getBoolean(KEY_COMPLETE); + isDeletingEventually = ParseJSONUtils.getInt(json, Arrays.asList( + KEY_IS_DELETING_EVENTUALLY, + KEY_IS_DELETING_EVENTUALLY_OLD + )); + JSONArray operations = json.getJSONArray(KEY_OPERATIONS); + { + ParseOperationSet newerOperations = currentOperations(); + operationSetQueue.clear(); + + // Add and enqueue any saveEventually operations, roll forward any other operation sets + // (operation sets here are generally failed/incomplete saves). + ParseOperationSet current = null; + for (int i = 0; i < operations.length(); i++) { + JSONObject operationSetJSON = operations.getJSONObject(i); + ParseOperationSet operationSet = ParseOperationSet.fromRest(operationSetJSON, decoder); + + if (operationSet.isSaveEventually()) { + if (current != null) { + operationSetQueue.add(current); + current = null; + } + saveEventuallyOperationSets.add(operationSet); + operationSetQueue.add(operationSet); + continue; + } + + if (current != null) { + operationSet.mergeFrom(current); + } + current = operationSet; + } + if (current != null) { + operationSetQueue.add(current); + } + + // Merge the changes that were previously in memory into the updated object. + currentOperations().mergeFrom(newerOperations); + } + + // We only want to merge server data if we our updatedAt is null (we're unsaved or from + // #createWithoutData) or if the JSON's updatedAt is newer than ours. + boolean mergeServerData = false; + if (state.updatedAt() < 0) { + mergeServerData = true; + } else if (json.has(KEY_UPDATED_AT)) { + Date otherUpdatedAt = ParseDateFormat.getInstance().parse(json.getString(KEY_UPDATED_AT)); + if (new Date(state.updatedAt()).compareTo(otherUpdatedAt) < 0) { + mergeServerData = true; + } + } + + if (mergeServerData) { + // Clean up internal json keys + JSONObject mergeJSON = ParseJSONUtils.create(json, Arrays.asList( + KEY_COMPLETE, KEY_IS_DELETING_EVENTUALLY, KEY_IS_DELETING_EVENTUALLY_OLD, + KEY_OPERATIONS + )); + State newState = mergeFromServer(state, mergeJSON, decoder, isComplete); + setState(newState); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + // We cannot modify the taskQueue inside synchronized (mutex). + for (ParseOperationSet operationSet : saveEventuallyOperationSets) { + enqueueSaveEventuallyOperationAsync(operationSet); + } + } + + //endregion + + private boolean hasDirtyChildren() { + synchronized (mutex) { + // We only need to consider the currently estimated children here, + // because they're the only ones that might need to be saved in a + // subsequent call to save, which is the meaning of "dirtiness". + List unsavedChildren = new ArrayList<>(); + collectDirtyChildren(estimatedData, unsavedChildren, null); + return unsavedChildren.size() > 0; + } + } + + /** + * Whether any key-value pair in this object (or its children) has been added/updated/removed and + * not saved yet. + * + * @return Whether this object has been altered and not saved yet. + */ + public boolean isDirty() { + return this.isDirty(true); + } + + /* package */ boolean isDirty(boolean considerChildren) { + synchronized (mutex) { + return (isDeleted || getObjectId() == null || hasChanges() || (considerChildren && hasDirtyChildren())); + } + } + + boolean hasChanges() { + synchronized (mutex) { + return currentOperations().size() > 0; + } + } + + /** + * Returns {@code true} if this {@code ParseObject} has operations in operationSetQueue that + * haven't been completed yet, {@code false} if there are no operations in the operationSetQueue. + */ + /* package */ boolean hasOutstandingOperations() { + synchronized (mutex) { + // > 1 since 1 is for unsaved changes. + return operationSetQueue.size() > 1; + } + } + + /** + * Whether a value associated with a key has been added/updated/removed and not saved yet. + * + * @param key + * The key to check for + * @return Whether this key has been altered and not saved yet. + */ + public boolean isDirty(String key) { + synchronized (mutex) { + return currentOperations().containsKey(key); + } + } + + /** + * Accessor to the object id. An object id is assigned as soon as an object is saved to the + * server. The combination of a className and an objectId uniquely identifies an object in your + * application. + * + * @return The object id. + */ + public String getObjectId() { + synchronized (mutex) { + return state.objectId(); + } + } + + /** + * Setter for the object id. In general you do not need to use this. However, in some cases this + * can be convenient. For example, if you are serializing a {@code ParseObject} yourself and wish + * to recreate it, you can use this to recreate the {@code ParseObject} exactly. + */ + public void setObjectId(String newObjectId) { + synchronized (mutex) { + String oldObjectId = state.objectId(); + if (ParseTextUtils.equals(oldObjectId, newObjectId)) { + return; + } + + // We don't need to use setState since it doesn't affect our cached state. + state = state.newBuilder().objectId(newObjectId).build(); + notifyObjectIdChanged(oldObjectId, newObjectId); + } + } + + /** + * Returns the localId, which is used internally for serializing relations to objects that don't + * yet have an objectId. + */ + /* package */ String getOrCreateLocalId() { + synchronized (mutex) { + if (localId == null) { + if (state.objectId() != null) { + throw new IllegalStateException( + "Attempted to get a localId for an object with an objectId."); + } + localId = getLocalIdManager().createLocalId(); + } + return localId; + } + } + + // Sets the objectId without marking dirty. + private void notifyObjectIdChanged(String oldObjectId, String newObjectId) { + synchronized (mutex) { + // The offline store may throw if this object already had a different objectId. + OfflineStore store = Parse.getLocalDatastore(); + if (store != null) { + store.updateObjectId(this, oldObjectId, newObjectId); + } + + if (localId != null) { + getLocalIdManager().setObjectId(localId, newObjectId); + localId = null; + } + } + } + + private ParseRESTObjectCommand currentSaveEventuallyCommand( + ParseOperationSet operations, ParseEncoder objectEncoder, String sessionToken) + throws ParseException { + State state = getState(); + + /* + * Get the JSON representation of the object, and use some of the information to construct the + * command. + */ + JSONObject objectJSON = toJSONObjectForSaving(state, operations, objectEncoder); + + ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand( + state, + objectJSON, + sessionToken); + return command; + } + + /** + * Converts a {@code ParseObject} to a JSON representation for saving to Parse. + * + *

+   * {
+   *   data: { // objectId plus any ParseFieldOperations },
+   *   classname: class name for the object
+   * }
+   * 
+ * + * updatedAt and createdAt are not included. only dirty keys are represented in the data. + * + * @see #mergeFromServer(State state, org.json.JSONObject, ParseDecoder, boolean) + */ + // Currently only used by saveEventually + /* package */ JSONObject toJSONObjectForSaving( + T state, ParseOperationSet operations, ParseEncoder objectEncoder) { + JSONObject objectJSON = new JSONObject(); + + try { + // Serialize the data + for (String key : operations.keySet()) { + ParseFieldOperation operation = operations.get(key); + objectJSON.put(key, objectEncoder.encode(operation)); + + // TODO(grantland): Use cached value from hashedObjects if it's a set operation. + } + + if (state.objectId() != null) { + objectJSON.put(KEY_OBJECT_ID, state.objectId()); + } + } catch (JSONException e) { + throw new RuntimeException("could not serialize object to JSON"); + } + + return objectJSON; + } + + /** + * Handles the result of {@code save}. + * + * Should be called on success or failure. + */ + // TODO(grantland): Remove once we convert saveEventually and ParseUser.signUp/resolveLaziness + // to controllers + /* package */ Task handleSaveResultAsync( + final JSONObject result, final ParseOperationSet operationsBeforeSave) { + ParseObject.State newState = null; + + if (result != null) { // Success + synchronized (mutex) { + final Map fetchedObjects = collectFetchedObjects(); + ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); + newState = ParseObjectCoder.get().decode(getState().newBuilder().clear(), result, decoder) + .isComplete(false) + .build(); + } + } + + return handleSaveResultAsync(newState, operationsBeforeSave); + } + + /** + * Handles the result of {@code save}. + * + * Should be called on success or failure. + */ + /* package */ Task handleSaveResultAsync( + final ParseObject.State result, final ParseOperationSet operationsBeforeSave) { + Task task = Task.forResult(null); + + final boolean success = result != null; + synchronized (mutex) { + // Find operationsBeforeSave in the queue so that we can remove it and move to the next + // operation set. + ListIterator opIterator = + operationSetQueue.listIterator(operationSetQueue.indexOf(operationsBeforeSave)); + opIterator.next(); + opIterator.remove(); + + if (!success) { + // Merge the data from the failed save into the next save. + ParseOperationSet nextOperation = opIterator.next(); + nextOperation.mergeFrom(operationsBeforeSave); + return task; + } + } + + /* + * If this object is in the offline store, then we need to make sure that we pull in any dirty + * changes it may have before merging the server data into it. + */ + final OfflineStore store = Parse.getLocalDatastore(); + if (store != null) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.fetchLocallyAsync(ParseObject.this).makeVoid(); + } + }); + } + + // fetchLocallyAsync will return an error if this object isn't in the LDS yet and that's ok + task = task.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + synchronized (mutex) { + State newState; + if (result.isComplete()) { + // Result is complete, so just replace + newState = result; + } else { + // Result is incomplete, so we'll need to apply it to the current state + newState = getState().newBuilder() + .apply(operationsBeforeSave) + .apply(result) + .build(); + } + setState(newState); + } + return null; + } + }); + + if (store != null) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.updateDataForObjectAsync(ParseObject.this); + } + }); + } + + task = task.onSuccess(new Continuation() { + @Override + public Void then(Task task) throws Exception { + saveEvent.invoke(ParseObject.this, null); + return null; + } + }); + + return task; + } + + /* package */ ParseOperationSet startSave() { + synchronized (mutex) { + ParseOperationSet currentOperations = currentOperations(); + operationSetQueue.addLast(new ParseOperationSet()); + return currentOperations; + } + } + + /* package */ void validateSave() { + // do nothing + } + + /** + * Saves this object to the server. Typically, you should use {@link #saveInBackground} instead of + * this, unless you are managing your own threading. + * + * @throws ParseException + * Throws an exception if the server is inaccessible. + */ + public final void save() throws ParseException { + ParseTaskUtils.wait(saveInBackground()); + } + + /** + * Saves this object to the server in a background thread. This is preferable to using {@link #save()}, + * unless your code is already running from a background thread. + * + * @return A {@link bolts.Task} that is resolved when the save completes. + */ + public final Task saveInBackground() { + return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser current = task.getResult(); + if (current == null) { + return Task.forResult(null); + } + if (!current.isLazy()) { + return Task.forResult(current.getSessionToken()); + } + + // The current user is lazy/unresolved. If it is attached to us via ACL, we'll need to + // resolve/save it before proceeding. + if (!isDataAvailable(KEY_ACL)) { + return Task.forResult(null); + } + final ParseACL acl = getACL(false); + if (acl == null) { + return Task.forResult(null); + } + final ParseUser user = acl.getUnresolvedUser(); + if (user == null || !user.isCurrentUser()) { + return Task.forResult(null); + } + return user.saveAsync(null).onSuccess(new Continuation() { + @Override + public String then(Task task) throws Exception { + if (acl.hasUnresolvedUser()) { + throw new IllegalStateException("ACL has an unresolved ParseUser. " + + "Save or sign up before attempting to serialize the ACL."); + } + return user.getSessionToken(); + } + }); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return saveAsync(sessionToken); + } + }); + } + + /* package */ Task saveAsync(final String sessionToken) { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return saveAsync(sessionToken, toAwait); + } + }); + } + + /* package */ Task saveAsync(final String sessionToken, final Task toAwait) { + if (!isDirty()) { + return Task.forResult(null); + } + + final ParseOperationSet operations; + synchronized (mutex) { + updateBeforeSave(); + validateSave(); + operations = startSave(); + } + + Task task; + synchronized (mutex) { + // Recursively save children + + /* + * TODO(klimt): Why is this estimatedData and not... I mean, what if a child is + * removed after save is called, but before the unresolved user gets resolved? It + * won't get saved. + */ + task = deepSaveAsync(estimatedData, sessionToken); + } + + return task.onSuccessTask( + TaskQueue.waitFor(toAwait) + ).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final Map fetchedObjects = collectFetchedObjects(); + ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); + return getObjectController().saveAsync(getState(), operations, sessionToken, decoder); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(final Task saveTask) throws Exception { + ParseObject.State result = saveTask.getResult(); + return handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted() || task.isCancelled()) { + return task; + } + + // We still want to propagate saveTask errors + return saveTask.makeVoid(); + } + }); + } + }); + } + + // Currently only used by ParsePinningEventuallyQueue for saveEventually due to the limitation in + // ParseCommandCache that it can only return JSONObject result. + /* package */ Task saveAsync( + ParseHttpClient client, + final ParseOperationSet operationSet, + String sessionToken) throws ParseException { + final ParseRESTCommand command = + currentSaveEventuallyCommand(operationSet, PointerEncoder.get(), sessionToken); + return command.executeAsync(client); + } + + /** + * Saves this object to the server in a background thread. This is preferable to using {@link #save()}, + * unless your code is already running from a background thread. + * + * @param callback + * {@code callback.done(e)} is called when the save completes. + */ + public final void saveInBackground(SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback); + } + + /* package */ void validateSaveEventually() throws ParseException { + // do nothing + } + + /** + * Saves this object to the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the save completes. If there is some problem with the object such that it + * can't be saved, it will be silently discarded. Objects saved with this method will be stored + * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately + * if possible. Otherwise, they will be sent the next time a network connection is available. + * Objects saved this way will persist even after the app is closed, in which case they will be + * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, + * subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old + * saves to be silently discarded until the connection can be re-established, and the queued + * objects can be saved. + * + * @param callback + * - A callback which will be called if the save completes before the app exits. + */ + public final void saveEventually(SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(saveEventually(), callback); + } + + /** + * Saves this object to the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the save completes. If there is some problem with the object such that it + * can't be saved, it will be silently discarded. Objects saved with this method will be stored + * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately + * if possible. Otherwise, they will be sent the next time a network connection is available. + * Objects saved this way will persist even after the app is closed, in which case they will be + * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, + * subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old + * saves to be silently discarded until the connection can be re-established, and the queued + * objects can be saved. + * + * @return A {@link bolts.Task} that is resolved when the save completes. + */ + public final Task saveEventually() { + if (!isDirty()) { + Parse.getEventuallyQueue().fakeObjectUpdate(); + return Task.forResult(null); + } + + final ParseOperationSet operationSet; + final ParseRESTCommand command; + final Task runEventuallyTask; + + synchronized (mutex) { + updateBeforeSave(); + try { + validateSaveEventually(); + } catch (ParseException e) { + return Task.forError(e); + } + + // TODO(klimt): Once we allow multiple saves on an object, this + // should be collecting dirty children from the estimate based on + // whatever data is going to be sent by this saveEventually, which + // won't necessarily be the current estimatedData. We should resolve + // this when the multiple save code is added. + List unsavedChildren = new ArrayList<>(); + collectDirtyChildren(estimatedData, unsavedChildren, null); + + String localId = null; + if (getObjectId() == null) { + localId = getOrCreateLocalId(); + } + + operationSet = startSave(); + operationSet.setIsSaveEventually(true); + + //TODO (grantland): Convert to async + final String sessionToken = ParseUser.getCurrentSessionToken(); + + try { + // See [1] + command = currentSaveEventuallyCommand(operationSet, PointerOrLocalIdEncoder.get(), + sessionToken); + + // TODO: Make this logic make sense once we have deepSaveEventually + command.setLocalId(localId); + + // Mark the command with a UUID so that we can match it up later. + command.setOperationSetUUID(operationSet.getUUID()); + + // Ensure local ids are retained before saveEventually-ing children + command.retainLocalIds(); + + for (ParseObject object : unsavedChildren) { + object.saveEventually(); + } + } catch (ParseException exception) { + throw new IllegalStateException("Unable to saveEventually.", exception); + } + } + + // We cannot modify the taskQueue inside synchronized (mutex). + ParseEventuallyQueue cache = Parse.getEventuallyQueue(); + runEventuallyTask = cache.enqueueEventuallyAsync(command, ParseObject.this); + enqueueSaveEventuallyOperationAsync(operationSet); + + // Release the extra retained local ids. + command.releaseLocalIds(); + + Task handleSaveResultTask; + if (Parse.isLocalDatastoreEnabled()) { + // ParsePinningEventuallyQueue calls handleSaveEventuallyResultAsync directly. + handleSaveResultTask = runEventuallyTask.makeVoid(); + } else { + handleSaveResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + JSONObject json = task.getResult(); + return handleSaveEventuallyResultAsync(json, operationSet); + } + }); + } + return handleSaveResultTask; + } + + /** + * Enqueues the saveEventually ParseOperationSet in {@link #taskQueue}. + */ + private Task enqueueSaveEventuallyOperationAsync(final ParseOperationSet operationSet) { + if (!operationSet.isSaveEventually()) { + throw new IllegalStateException( + "This should only be used to enqueue saveEventually operation sets"); + } + + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseEventuallyQueue cache = Parse.getEventuallyQueue(); + return cache.waitForOperationSetAndEventuallyPin(operationSet, null).makeVoid(); + } + }); + } + }); + } + + /** + * Handles the result of {@code saveEventually}. + * + * In addition to normal save handling, this also notifies the saveEventually test helper. + * + * Should be called on success or failure. + */ + /* package */ Task handleSaveEventuallyResultAsync( + JSONObject json, ParseOperationSet operationSet) { + final boolean success = json != null; + Task handleSaveResultTask = handleSaveResultAsync(json, operationSet); + + return handleSaveResultTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (success) { + Parse.getEventuallyQueue() + .notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_UPDATED); + } + return task; + } + }); + } + + /** + * Called by {@link #saveInBackground()} and {@link #saveEventually(SaveCallback)} + * and guaranteed to be thread-safe. Subclasses can override this method to do any custom updates + * before an object gets saved. + */ + /* package */ void updateBeforeSave() { + // do nothing + } + + /** + * Deletes this object from the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the delete completes. If there is some problem with the object such that it + * can't be deleted, the request will be silently discarded. Delete requests made with this method + * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be + * sent immediately if possible. Otherwise, they will be sent the next time a network connection + * is available. Delete instructions saved this way will persist even after the app is closed, in + * which case they will be sent the next time the app is opened. If more than 10MB of commands are + * waiting to be sent, subsequent calls to {@code #deleteEventually()} or + * {@link #saveEventually()} will cause old instructions to be silently discarded until the + * connection can be re-established, and the queued objects can be saved. + * + * @param callback + * - A callback which will be called if the delete completes before the app exits. + */ + public final void deleteEventually(DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteEventually(), callback); + } + + /** + * Deletes this object from the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the delete completes. If there is some problem with the object such that it + * can't be deleted, the request will be silently discarded. Delete requests made with this method + * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be + * sent immediately if possible. Otherwise, they will be sent the next time a network connection + * is available. Delete instructions saved this way will persist even after the app is closed, in + * which case they will be sent the next time the app is opened. If more than 10MB of commands are + * waiting to be sent, subsequent calls to {@code #deleteEventually()} or + * {@link #saveEventually()} will cause old instructions to be silently discarded until the + * connection can be re-established, and the queued objects can be saved. + * + * @return A {@link bolts.Task} that is resolved when the delete completes. + */ + public final Task deleteEventually() { + final ParseRESTCommand command; + final Task runEventuallyTask; + synchronized (mutex) { + validateDelete(); + isDeletingEventually += 1; + + String localId = null; + if (getObjectId() == null) { + localId = getOrCreateLocalId(); + } + + // TODO(grantland): Convert to async + final String sessionToken = ParseUser.getCurrentSessionToken(); + + // See [1] + command = ParseRESTObjectCommand.deleteObjectCommand( + getState(), sessionToken); + command.setLocalId(localId); + + runEventuallyTask = Parse.getEventuallyQueue().enqueueEventuallyAsync(command, ParseObject.this); + } + + Task handleDeleteResultTask; + if (Parse.isLocalDatastoreEnabled()) { + // ParsePinningEventuallyQueue calls handleDeleteEventuallyResultAsync directly. + handleDeleteResultTask = runEventuallyTask.makeVoid(); + } else { + handleDeleteResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return handleDeleteEventuallyResultAsync(); + } + }); + } + + return handleDeleteResultTask; + } + + /** + * Handles the result of {@code deleteEventually}. + * + * Should only be called on success. + */ + /* package */ Task handleDeleteEventuallyResultAsync() { + synchronized (mutex) { + isDeletingEventually -= 1; + } + Task handleDeleteResultTask = handleDeleteResultAsync(); + + return handleDeleteResultTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + Parse.getEventuallyQueue() + .notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_REMOVED); + return task; + } + }); + } + + /** + * Handles the result of {@code fetch}. + * + * Should only be called on success. + */ + /* package */ Task handleFetchResultAsync(final ParseObject.State result) { + Task task = Task.forResult(null); + + /* + * If this object is in the offline store, then we need to make sure that we pull in any dirty + * changes it may have before merging the server data into it. + */ + final OfflineStore store = Parse.getLocalDatastore(); + if (store != null) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.fetchLocallyAsync(ParseObject.this).makeVoid(); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Catch CACHE_MISS + if (task.getError() instanceof ParseException + && ((ParseException)task.getError()).getCode() == ParseException.CACHE_MISS) { + return null; + } + return task; + } + }); + } + + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (mutex) { + State newState; + if (result.isComplete()) { + // Result is complete, so just replace + newState = result; + } else { + // Result is incomplete, so we'll need to apply it to the current state + newState = getState().newBuilder().apply(result).build(); + } + setState(newState); + } + return null; + } + }); + + if (store != null) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return store.updateDataForObjectAsync(ParseObject.this); + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Catch CACHE_MISS + if (task.getError() instanceof ParseException + && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) { + return null; + } + return task; + } + }); + } + + return task; + } + + /** + * Refreshes this object with the data from the server. Call this whenever you want the state of + * the object to reflect exactly what is on the server. + * + * @throws ParseException + * Throws an exception if the server is inaccessible. + * + * @deprecated Please use {@link #fetch()} instead. + */ + @Deprecated + public final void refresh() throws ParseException { + fetch(); + } + + /** + * Refreshes this object with the data from the server in a background thread. This is preferable + * to using refresh(), unless your code is already running from a background thread. + * + * @param callback + * {@code callback.done(object, e)} is called when the refresh completes. + * + * @deprecated Please use {@link #fetchInBackground(GetCallback)} instead. + */ + @Deprecated + public final void refreshInBackground(RefreshCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(fetchInBackground(), callback); + } + + /** + * Fetches this object with the data from the server. Call this whenever you want the state of the + * object to reflect exactly what is on the server. + * + * @throws ParseException + * Throws an exception if the server is inaccessible. + * @return The {@code ParseObject} that was fetched. + */ + public T fetch() throws ParseException { + return ParseTaskUtils.wait(this.fetchInBackground()); + } + + @SuppressWarnings("unchecked") + /* package */ Task fetchAsync( + final String sessionToken, Task toAwait) { + return toAwait.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + State state; + Map fetchedObjects; + synchronized (mutex) { + state = getState(); + fetchedObjects = collectFetchedObjects(); + } + ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); + return getObjectController().fetchAsync(state, sessionToken, decoder); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseObject.State result = task.getResult(); + return handleFetchResultAsync(result); + } + }).onSuccess(new Continuation() { + @Override + public T then(Task task) throws Exception { + return (T) ParseObject.this; + } + }); + } + + /** + * Fetches this object with the data from the server in a background thread. This is preferable to + * using fetch(), unless your code is already running from a background thread. + * + * @return A {@link bolts.Task} that is resolved when fetch completes. + */ + public final Task fetchInBackground() { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return fetchAsync(sessionToken, toAwait); + } + }); + } + }); + } + + /** + * Fetches this object with the data from the server in a background thread. This is preferable to + * using fetch(), unless your code is already running from a background thread. + * + * @param callback + * {@code callback.done(object, e)} is called when the fetch completes. + */ + public final void fetchInBackground(GetCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(this.fetchInBackground(), callback); + } + + /** + * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), + * fetches this object with the data from the server in a background thread. This is preferable to + * using {@link #fetchIfNeeded()}, unless your code is already running from a background thread. + * + * @return A {@link bolts.Task} that is resolved when fetch completes. + */ + public final Task fetchIfNeededInBackground() { + if (isDataAvailable()) { + return Task.forResult((T) this); + } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + if (isDataAvailable()) { + return Task.forResult((T) ParseObject.this); + } + return fetchAsync(sessionToken, toAwait); + } + }); + } + }); + + } + + /** + * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), + * fetches this object with the data from the server. + * + * @throws ParseException + * Throws an exception if the server is inaccessible. + * @return The fetched {@code ParseObject}. + */ + public T fetchIfNeeded() throws ParseException { + return ParseTaskUtils.wait(this.fetchIfNeededInBackground()); + } + + /** + * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), + * fetches this object with the data from the server in a background thread. This is preferable to + * using {@link #fetchIfNeeded()}, unless your code is already running from a background thread. + * + * @param callback + * {@code callback.done(object, e)} is called when the fetch completes. + */ + public final void fetchIfNeededInBackground(GetCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(this.fetchIfNeededInBackground(), callback); + } + + // Validates the delete method + /* package */ void validateDelete() { + // do nothing + } + + private Task deleteAsync(final String sessionToken, Task toAwait) { + validateDelete(); + + return toAwait.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + isDeleting = true; + if (state.objectId() == null) { + return task.cast(); // no reason to call delete since it doesn't exist + } + return deleteAsync(sessionToken); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return handleDeleteResultAsync(); + } + }).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + isDeleting = false; + return null; + } + }); + } + + //TODO (grantland): I'm not sure we want direct access to this. All access to `delete` should + // enqueue on the taskQueue... + /* package */ Task deleteAsync(String sessionToken) throws ParseException { + return getObjectController().deleteAsync(getState(), sessionToken); + } + + /** + * Handles the result of {@code delete}. + * + * Should only be called on success. + */ + /* package */ Task handleDeleteResultAsync() { + Task task = Task.forResult(null); + + synchronized (mutex) { + isDeleted = true; + } + + final OfflineStore store = Parse.getLocalDatastore(); + if (store != null) { + task = task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (mutex) { + if (isDeleted) { + store.unregisterObject(ParseObject.this); + return store.deleteDataForObjectAsync(ParseObject.this); + } else { + return store.updateDataForObjectAsync(ParseObject.this); + } + } + } + }); + } + + return task; + } + + /** + * Deletes this object on the server in a background thread. This is preferable to using + * {@link #delete()}, unless your code is already running from a background thread. + * + * @return A {@link bolts.Task} that is resolved when delete completes. + */ + public final Task deleteInBackground() { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return deleteAsync(sessionToken, toAwait); + } + }); + } + }); + } + + /** + * Deletes this object on the server. This does not delete or destroy the object locally. + * + * @throws ParseException + * Throws an error if the object does not exist or if the internet fails. + */ + public final void delete() throws ParseException { + ParseTaskUtils.wait(deleteInBackground()); + } + + /** + * Deletes this object on the server in a background thread. This is preferable to using + * {@link #delete()}, unless your code is already running from a background thread. + * + * @param callback + * {@code callback.done(e)} is called when the save completes. + */ + public final void deleteInBackground(DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteInBackground(), callback); + } + + /** + * This deletes all of the objects from the given List. + */ + private static Task deleteAllAsync( + final List objects, final String sessionToken) { + if (objects.size() == 0) { + return Task.forResult(null); + } + + // Create a list of unique objects based on objectIds + int objectCount = objects.size(); + final List uniqueObjects = new ArrayList<>(objectCount); + final HashSet idSet = new HashSet<>(); + for (int i = 0; i < objectCount; i++) { + ParseObject obj = objects.get(i); + if (!idSet.contains(obj.getObjectId())) { + idSet.add(obj.getObjectId()); + uniqueObjects.add(obj); + } + } + + return enqueueForAll(uniqueObjects, new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return deleteAllAsync(uniqueObjects, sessionToken, toAwait); + } + }); + } + + private static Task deleteAllAsync( + final List uniqueObjects, final String sessionToken, Task toAwait) { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + int objectCount = uniqueObjects.size(); + List states = new ArrayList<>(objectCount); + for (int i = 0; i < objectCount; i++) { + ParseObject object = uniqueObjects.get(i); + object.validateDelete(); + states.add(object.getState()); + } + List> batchTasks = getObjectController().deleteAllAsync(states, sessionToken); + + List> tasks = new ArrayList<>(objectCount); + for (int i = 0; i < objectCount; i++) { + Task batchTask = batchTasks.get(i); + final T object = uniqueObjects.get(i); + tasks.add(batchTask.onSuccessTask(new Continuation>() { + @Override + public Task then(final Task batchTask) throws Exception { + return object.handleDeleteResultAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return batchTask; + } + }); + } + })); + } + return Task.whenAll(tasks); + } + }); + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects + * The objects to delete. + * @throws ParseException + * Throws an exception if the server returns an error or is inaccessible. + */ + public static void deleteAll(List objects) throws ParseException { + ParseTaskUtils.wait(deleteAllInBackground(objects)); + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects + * The objects to delete. + * @param callback + * The callback method to execute when completed. + */ + public static void deleteAllInBackground(List objects, DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteAllInBackground(objects), callback); + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects + * The objects to delete. + * + * @return A {@link bolts.Task} that is resolved when deleteAll completes. + */ + public static Task deleteAllInBackground(final List objects) { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + return deleteAllAsync(objects, sessionToken); + } + }); + } + + /** + * Finds all of the objects that are reachable from child, including child itself, and adds them + * to the given mutable array. It traverses arrays and json objects. + * + * @param node + * An kind object to search for children. + * @param dirtyChildren + * The array to collect the {@code ParseObject}s into. + * @param dirtyFiles + * The array to collect the {@link ParseFile}s into. + * @param alreadySeen + * The set of all objects that have already been seen. + * @param alreadySeenNew + * The set of new objects that have already been seen since the last existing object. + */ + private static void collectDirtyChildren(Object node, + final Collection dirtyChildren, + final Collection dirtyFiles, + final Set alreadySeen, + final Set alreadySeenNew) { + + new ParseTraverser() { + @Override + protected boolean visit(Object node) { + // If it's a file, then add it to the list if it's dirty. + if (node instanceof ParseFile) { + if (dirtyFiles == null) { + return true; + } + + ParseFile file = (ParseFile) node; + if (file.getUrl() == null) { + dirtyFiles.add(file); + } + return true; + } + + // If it's anything other than a file, then just continue; + if (!(node instanceof ParseObject)) { + return true; + } + + if (dirtyChildren == null) { + return true; + } + + // For files, we need to handle recursion manually to find cycles of new objects. + ParseObject object = (ParseObject) node; + Set seen = alreadySeen; + Set seenNew = alreadySeenNew; + + // Check for cycles of new objects. Any such cycle means it will be + // impossible to save this collection of objects, so throw an exception. + if (object.getObjectId() != null) { + seenNew = new HashSet<>(); + } else { + if (seenNew.contains(object)) { + throw new RuntimeException("Found a circular dependency while saving."); + } + seenNew = new HashSet<>(seenNew); + seenNew.add(object); + } + + // Check for cycles of any object. If this occurs, then there's no + // problem, but we shouldn't recurse any deeper, because it would be + // an infinite recursion. + if (seen.contains(object)) { + return true; + } + seen = new HashSet<>(seen); + seen.add(object); + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + collectDirtyChildren(object.estimatedData, dirtyChildren, dirtyFiles, seen, seenNew); + + if (object.isDirty(false)) { + dirtyChildren.add(object); + } + + return true; + } + }.setYieldRoot(true).traverse(node); + } + + /** + * Helper version of collectDirtyChildren so that callers don't have to add the internally used + * parameters. + */ + private static void collectDirtyChildren(Object node, Collection dirtyChildren, + Collection dirtyFiles) { + collectDirtyChildren(node, dirtyChildren, dirtyFiles, + new HashSet(), + new HashSet()); + } + + /** + * Returns {@code true} if this object can be serialized for saving. + */ + private boolean canBeSerialized() { + synchronized (mutex) { + final Capture result = new Capture<>(true); + + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + new ParseTraverser() { + @Override + protected boolean visit(Object value) { + if (value instanceof ParseFile) { + ParseFile file = (ParseFile) value; + if (file.isDirty()) { + result.set(false); + } + } + + if (value instanceof ParseObject) { + ParseObject object = (ParseObject) value; + if (object.getObjectId() == null) { + result.set(false); + } + } + + // Continue to traverse only if it can still be serialized. + return result.get(); + } + }.setYieldRoot(false).setTraverseParseObjects(true).traverse(this); + + return result.get(); + } + } + + /** + * This saves all of the objects and files reachable from the given object. It does its work in + * multiple waves, saving as many as possible in each wave. If there's ever an error, it just + * gives up, sets error, and returns NO. + */ + private static Task deepSaveAsync(final Object object, final String sessionToken) { + Set objects = new HashSet<>(); + Set files = new HashSet<>(); + collectDirtyChildren(object, objects, files); + + // This has to happen separately from everything else because ParseUser.save() is + // special-cased to work for lazy users, but new users can't be created by + // ParseMultiCommand's regular save. + Set users = new HashSet<>(); + for (ParseObject o : objects) { + if (o instanceof ParseUser) { + ParseUser user = (ParseUser) o; + if (user.isLazy()) { + users.add((ParseUser) o); + } + } + } + objects.removeAll(users); + + // objects will need to wait for files to be complete since they may be nested children. + final AtomicBoolean filesComplete = new AtomicBoolean(false); + List> tasks = new ArrayList<>(); + for (ParseFile file : files) { + tasks.add(file.saveAsync(sessionToken, null, null)); + } + Task filesTask = Task.whenAll(tasks).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + filesComplete.set(true); + return null; + } + }); + + // objects will need to wait for users to be complete since they may be nested children. + final AtomicBoolean usersComplete = new AtomicBoolean(false); + tasks = new ArrayList<>(); + for (final ParseUser user : users) { + tasks.add(user.saveAsync(sessionToken)); + } + Task usersTask = Task.whenAll(tasks).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + usersComplete.set(true); + return null; + } + }); + + final Capture> remaining = new Capture<>(objects); + Task objectsTask = Task.forResult(null).continueWhile(new Callable() { + @Override + public Boolean call() throws Exception { + return remaining.get().size() > 0; + } + }, new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Partition the objects into two sets: those that can be save immediately, + // and those that rely on other objects to be created first. + final List current = new ArrayList<>(); + final Set nextBatch = new HashSet<>(); + for (ParseObject obj : remaining.get()) { + if (obj.canBeSerialized()) { + current.add(obj); + } else { + nextBatch.add(obj); + } + } + remaining.set(nextBatch); + + if (current.size() == 0 && filesComplete.get() && usersComplete.get()) { + // We do cycle-detection when building the list of objects passed to this function, so + // this should never get called. But we should check for it anyway, so that we get an + // exception instead of an infinite loop. + throw new RuntimeException("Unable to save a ParseObject with a relation to a cycle."); + } + + // Package all save commands together + if (current.size() == 0) { + return Task.forResult(null); + } + + return enqueueForAll(current, new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return saveAllAsync(current, sessionToken, toAwait); + } + }); + } + }); + + return Task.whenAll(Arrays.asList(filesTask, usersTask, objectsTask)); + } + + private static Task saveAllAsync( + final List uniqueObjects, final String sessionToken, Task toAwait) { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + int objectCount = uniqueObjects.size(); + List states = new ArrayList<>(objectCount); + List operationsList = new ArrayList<>(objectCount); + List decoders = new ArrayList<>(objectCount); + for (int i = 0; i < objectCount; i++) { + ParseObject object = uniqueObjects.get(i); + object.updateBeforeSave(); + object.validateSave(); + + states.add(object.getState()); + operationsList.add(object.startSave()); + final Map fetchedObjects = object.collectFetchedObjects(); + decoders.add(new KnownParseObjectDecoder(fetchedObjects)); + } + List> batchTasks = getObjectController().saveAllAsync( + states, operationsList, sessionToken, decoders); + + List> tasks = new ArrayList<>(objectCount); + for (int i = 0; i < objectCount; i++) { + Task batchTask = batchTasks.get(i); + final T object = uniqueObjects.get(i); + final ParseOperationSet operations = operationsList.get(i); + tasks.add(batchTask.continueWithTask(new Continuation>() { + @Override + public Task then(final Task batchTask) throws Exception { + ParseObject.State result = batchTask.getResult(); // will be null on failure + return object.handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted() || task.isCancelled()) { + return task; + } + + // We still want to propagate batchTask errors + return batchTask.makeVoid(); + } + }); + } + })); + } + return Task.whenAll(tasks); + } + }); + } + + /** + * Saves each object in the provided list. This is faster than saving each object individually + * because it batches the requests. + * + * @param objects + * The objects to save. + * @throws ParseException + * Throws an exception if the server returns an error or is inaccessible. + */ + public static void saveAll(List objects) throws ParseException { + ParseTaskUtils.wait(saveAllInBackground(objects)); + } + + /** + * Saves each object in the provided list to the server in a background thread. This is preferable + * to using saveAll, unless your code is already running from a background thread. + * + * @param objects + * The objects to save. + * @param callback + * {@code callback.done(e)} is called when the save completes. + */ + public static void saveAllInBackground(List objects, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(saveAllInBackground(objects), callback); + } + + /** + * Saves each object in the provided list to the server in a background thread. This is preferable + * to using saveAll, unless your code is already running from a background thread. + * + * @param objects + * The objects to save. + * + * @return A {@link bolts.Task} that is resolved when saveAll completes. + */ + public static Task saveAllInBackground(final List objects) { + return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser current = task.getResult(); + if (current == null) { + return Task.forResult(null); + } + if (!current.isLazy()) { + return Task.forResult(current.getSessionToken()); + } + + // The current user is lazy/unresolved. If it is attached to any of the objects via ACL, + // we'll need to resolve/save it before proceeding. + for (ParseObject object : objects) { + if (!object.isDataAvailable(KEY_ACL)) { + continue; + } + final ParseACL acl = object.getACL(false); + if (acl == null) { + continue; + } + final ParseUser user = acl.getUnresolvedUser(); + if (user != null && user.isCurrentUser()) { + // We only need to find one, since there's only one current user. + return user.saveAsync(null).onSuccess(new Continuation() { + @Override + public String then(Task task) throws Exception { + if (acl.hasUnresolvedUser()) { + throw new IllegalStateException("ACL has an unresolved ParseUser. " + + "Save or sign up before attempting to serialize the ACL."); + } + return user.getSessionToken(); + } + }); + } + } + + // There were no objects with ACLs pointing to unresolved users. + return Task.forResult(null); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final String sessionToken = task.getResult(); + return deepSaveAsync(objects, sessionToken); + } + }); + } + + /** + * Fetches all the objects that don't have data in the provided list in the background. + * + * @param objects + * The list of objects to fetch. + * + * @return A {@link bolts.Task} that is resolved when fetchAllIfNeeded completes. + */ + public static Task> fetchAllIfNeededInBackground( + final List objects) { + return fetchAllAsync(objects, true); + } + + /** + * Fetches all the objects that don't have data in the provided list. + * + * @param objects + * The list of objects to fetch. + * @return The list passed in for convenience. + * @throws ParseException + * Throws an exception if the server returns an error or is inaccessible. + */ + public static List fetchAllIfNeeded(List objects) + throws ParseException { + return ParseTaskUtils.wait(fetchAllIfNeededInBackground(objects)); + } + + /** + * Fetches all the objects that don't have data in the provided list in the background. + * + * @param objects + * The list of objects to fetch. + * @param callback + * {@code callback.done(result, e)} is called when the fetch completes. + */ + public static void fetchAllIfNeededInBackground(final List objects, + FindCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(fetchAllIfNeededInBackground(objects), callback); + } + + private static Task> fetchAllAsync( + final List objects, final boolean onlyIfNeeded) { + return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + final ParseUser user = task.getResult(); + return enqueueForAll(objects, new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + return fetchAllAsync(objects, user, onlyIfNeeded, task); + } + }); + } + }); + } + + /** + * @param onlyIfNeeded If enabled, will only fetch if the object has an objectId and + * !isDataAvailable, otherwise it requires objectIds and will fetch regardless + * of data availability. + */ + // TODO(grantland): Convert to ParseUser.State + private static Task> fetchAllAsync( + final List objects, final ParseUser user, final boolean onlyIfNeeded, Task toAwait) { + if (objects.size() == 0) { + return Task.forResult(objects); + } + + List objectIds = new ArrayList<>(); + String className = null; + for (T object : objects) { + if (onlyIfNeeded && object.isDataAvailable()) { + continue; + } + + if (className != null && !object.getClassName().equals(className)) { + throw new IllegalArgumentException("All objects should have the same class"); + } + className = object.getClassName(); + + String objectId = object.getObjectId(); + if (objectId != null) { + objectIds.add(object.getObjectId()); + } else if (!onlyIfNeeded) { + throw new IllegalArgumentException("All objects must exist on the server"); + } + } + + if (objectIds.size() == 0) { + return Task.forResult(objects); + } + + final ParseQuery query = ParseQuery.getQuery(className) + .whereContainedIn(KEY_OBJECT_ID, objectIds); + return toAwait.continueWithTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + return query.findAsync(query.getBuilder().build(), user, null); + } + }).onSuccess(new Continuation, List>() { + @Override + public List then(Task> task) throws Exception { + Map resultMap = new HashMap<>(); + for (T o : task.getResult()) { + resultMap.put(o.getObjectId(), o); + } + for (T object : objects) { + if (onlyIfNeeded && object.isDataAvailable()) { + continue; + } + + T newObject = resultMap.get(object.getObjectId()); + if (newObject == null) { + throw new ParseException( + ParseException.OBJECT_NOT_FOUND, + "Object id " + object.getObjectId() + " does not exist"); + } + if (!Parse.isLocalDatastoreEnabled()) { + // We only need to merge if LDS is disabled, since single instance will do the merging + // for us. + object.mergeFromObject(newObject); + } + } + return objects; + } + }); + } + + /** + * Fetches all the objects in the provided list in the background. + * + * @param objects + * The list of objects to fetch. + * + * @return A {@link bolts.Task} that is resolved when fetch completes. + */ + public static Task> fetchAllInBackground(final List objects) { + return fetchAllAsync(objects, false); + } + + /** + * Fetches all the objects in the provided list. + * + * @param objects + * The list of objects to fetch. + * @return The list passed in. + * @throws ParseException + * Throws an exception if the server returns an error or is inaccessible. + */ + public static List fetchAll(List objects) throws ParseException { + return ParseTaskUtils.wait(fetchAllInBackground(objects)); + } + + /** + * Fetches all the objects in the provided list in the background. + * + * @param objects + * The list of objects to fetch. + * @param callback + * {@code callback.done(result, e)} is called when the fetch completes. + */ + public static void fetchAllInBackground(List objects, + FindCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(fetchAllInBackground(objects), callback); + } + + /** + * Return the operations that will be sent in the next call to save. + */ + private ParseOperationSet currentOperations() { + synchronized (mutex) { + return operationSetQueue.getLast(); + } + } + + /** + * Updates the estimated values in the map based on the given set of ParseFieldOperations. + */ + private void applyOperations(ParseOperationSet operations, Map map) { + for (String key : operations.keySet()) { + ParseFieldOperation operation = operations.get(key); + Object oldValue = map.get(key); + Object newValue = operation.apply(oldValue, key); + if (newValue != null) { + map.put(key, newValue); + } else { + map.remove(key); + } + } + } + + /** + * Regenerates the estimatedData map from the serverData and operations. + */ + private void rebuildEstimatedData() { + synchronized (mutex) { + estimatedData.clear(); + for (String key : state.keySet()) { + estimatedData.put(key, state.get(key)); + } + for (ParseOperationSet operations : operationSetQueue) { + applyOperations(operations, estimatedData); + } + } + } + + /* package */ void markAllFieldsDirty() { + synchronized (mutex) { + for (String key : state.keySet()) { + performPut(key, state.get(key)); + } + } + } + + /** + * performOperation() is like {@link #put(String, Object)} but instead of just taking a new value, + * it takes a ParseFieldOperation that modifies the value. + */ + /* package */ void performOperation(String key, ParseFieldOperation operation) { + synchronized (mutex) { + Object oldValue = estimatedData.get(key); + Object newValue = operation.apply(oldValue, key); + if (newValue != null) { + estimatedData.put(key, newValue); + } else { + estimatedData.remove(key); + } + + ParseFieldOperation oldOperation = currentOperations().get(key); + ParseFieldOperation newOperation = operation.mergeWithPrevious(oldOperation); + currentOperations().put(key, newOperation); + } + } + + /** + * Add a key-value pair to this object. It is recommended to name keys in + * camelCaseLikeThis. + * + * @param key + * Keys must be alphanumerical plus underscore, and start with a letter. + * @param value + * Values may be numerical, {@link String}, {@link JSONObject}, {@link JSONArray}, + * {@link JSONObject#NULL}, or other {@code ParseObject}s. value may not be {@code null}. + */ + public void put(String key, Object value) { + checkKeyIsMutable(key); + + performPut(key, value); + } + + /* package */ void performPut(String key, Object value) { + if (key == null) { + throw new IllegalArgumentException("key may not be null."); + } + + if (value == null) { + throw new IllegalArgumentException("value may not be null."); + } + + if (value instanceof JSONObject) { + ParseDecoder decoder = ParseDecoder.get(); + value = decoder.convertJSONObjectToMap((JSONObject) value); + } else if (value instanceof JSONArray) { + ParseDecoder decoder = ParseDecoder.get(); + value = decoder.convertJSONArrayToList((JSONArray) value); + } + + if (!ParseEncoder.isValidType(value)) { + throw new IllegalArgumentException("invalid type for value: " + value.getClass().toString()); + } + + performOperation(key, new ParseSetOperation(value)); + } + + /** + * Whether this object has a particular key. Same as {@link #containsKey(String)}. + * + * @param key + * The key to check for + * @return Whether this object contains the key + */ + public boolean has(String key) { + return containsKey(key); + } + + /** + * Atomically increments the given key by 1. + * + * @param key + * The key to increment. + */ + public void increment(String key) { + increment(key, 1); + } + + /** + * Atomically increments the given key by the given number. + * + * @param key + * The key to increment. + * @param amount + * The amount to increment by. + */ + public void increment(String key, Number amount) { + ParseIncrementOperation operation = new ParseIncrementOperation(amount); + performOperation(key, operation); + } + + /** + * Atomically adds an object to the end of the array associated with a given key. + * + * @param key + * The key. + * @param value + * The object to add. + */ + public void add(String key, Object value) { + this.addAll(key, Arrays.asList(value)); + } + + /** + * Atomically adds the objects contained in a {@code Collection} to the end of the array + * associated with a given key. + * + * @param key + * The key. + * @param values + * The objects to add. + */ + public void addAll(String key, Collection values) { + ParseAddOperation operation = new ParseAddOperation(values); + performOperation(key, operation); + } + + /** + * Atomically adds an object to the array associated with a given key, only if it is not already + * present in the array. The position of the insert is not guaranteed. + * + * @param key + * The key. + * @param value + * The object to add. + */ + public void addUnique(String key, Object value) { + this.addAllUnique(key, Arrays.asList(value)); + } + + /** + * Atomically adds the objects contained in a {@code Collection} to the array associated with a + * given key, only adding elements which are not already present in the array. The position of the + * insert is not guaranteed. + * + * @param key + * The key. + * @param values + * The objects to add. + */ + public void addAllUnique(String key, Collection values) { + ParseAddUniqueOperation operation = new ParseAddUniqueOperation(values); + performOperation(key, operation); + } + + /** + * Removes a key from this object's data if it exists. + * + * @param key + * The key to remove. + */ + public void remove(String key) { + checkKeyIsMutable(key); + + performRemove(key); + } + + /* package */ void performRemove(String key) { + synchronized (mutex) { + Object object = get(key); + + if (object != null) { + performOperation(key, ParseDeleteOperation.getInstance()); + } + } + } + + /** + * Atomically removes all instances of the objects contained in a {@code Collection} from the + * array associated with a given key. To maintain consistency with the Java Collection API, there + * is no method removing all instances of a single object. Instead, you can call + * {@code parseObject.removeAll(key, Arrays.asList(value))}. + * + * @param key + * The key. + * @param values + * The objects to remove. + */ + public void removeAll(String key, Collection values) { + checkKeyIsMutable(key); + + ParseRemoveOperation operation = new ParseRemoveOperation(values); + performOperation(key, operation); + } + + private void checkKeyIsMutable(String key) { + if (!isKeyMutable(key)) { + throw new IllegalArgumentException("Cannot modify `" + key + + "` property of an " + getClassName() + " object."); + } + } + + /* package */ boolean isKeyMutable(String key) { + return true; + } + + /** + * Whether this object has a particular key. Same as {@link #has(String)}. + * + * @param key + * The key to check for + * @return Whether this object contains the key + */ + public boolean containsKey(String key) { + synchronized (mutex) { + return estimatedData.containsKey(key); + } + } + + + /** + * Access a {@link String} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link String}. + */ + public String getString(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof String)) { + return null; + } + return (String) value; + } + } + + /** + * Access a {@code byte[]} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@code byte[]}. + */ + public byte[] getBytes(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof byte[])) { + return null; + } + + return (byte[]) value; + } + } + + /** + * Access a {@link Number} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link Number}. + */ + public Number getNumber(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof Number)) { + return null; + } + return (Number) value; + } + } + + /** + * Access a {@link JSONArray} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link JSONArray}. + */ + public JSONArray getJSONArray(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + + if (value instanceof List) { + value = PointerOrLocalIdEncoder.get().encode(value); + } + + if (!(value instanceof JSONArray)) { + return null; + } + return (JSONArray) value; + } + } + + /** + * Access a {@link List} value. + * + * @param key + * The key to access the value for + * @return {@code null} if there is no such key or if the value can't be converted to a + * {@link List}. + */ + public List getList(String key) { + synchronized (mutex) { + Object value = estimatedData.get(key); + if (!(value instanceof List)) { + return null; + } + @SuppressWarnings("unchecked") + List returnValue = (List) value; + return returnValue; + } + } + + /** + * Access a {@link Map} value + * + * @param key + * The key to access the value for + * @return {@code null} if there is no such key or if the value can't be converted to a + * {@link Map}. + */ + public Map getMap(String key) { + synchronized (mutex) { + Object value = estimatedData.get(key); + if (!(value instanceof Map)) { + return null; + } + @SuppressWarnings("unchecked") + Map returnValue = (Map) value; + return returnValue; + } + } + + /** + * Access a {@link JSONObject} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link JSONObject}. + */ + public JSONObject getJSONObject(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + + if (value instanceof Map) { + value = PointerOrLocalIdEncoder.get().encode(value); + } + + if (!(value instanceof JSONObject)) { + return null; + } + + return (JSONObject) value; + } + } + + /** + * Access an {@code int} value. + * + * @param key + * The key to access the value for. + * @return {@code 0} if there is no such key or if it is not a {@code int}. + */ + public int getInt(String key) { + Number number = getNumber(key); + if (number == null) { + return 0; + } + return number.intValue(); + } + + /** + * Access a {@code double} value. + * + * @param key + * The key to access the value for. + * @return {@code 0} if there is no such key or if it is not a {@code double}. + */ + public double getDouble(String key) { + Number number = getNumber(key); + if (number == null) { + return 0; + } + return number.doubleValue(); + } + + /** + * Access a {@code long} value. + * + * @param key + * The key to access the value for. + * @return {@code 0} if there is no such key or if it is not a {@code long}. + */ + public long getLong(String key) { + Number number = getNumber(key); + if (number == null) { + return 0; + } + return number.longValue(); + } + + /** + * Access a {@code boolean} value. + * + * @param key + * The key to access the value for. + * @return {@code false} if there is no such key or if it is not a {@code boolean}. + */ + public boolean getBoolean(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof Boolean)) { + return false; + } + return (Boolean) value; + } + } + + /** + * Access a {@link Date} value. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link Date}. + */ + public Date getDate(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof Date)) { + return null; + } + return (Date) value; + } + } + + /** + * Access a {@code ParseObject} value. This function will not perform a network request. Unless the + * {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling + * {@link #fetchIfNeeded()} or {@link #refresh()}), {@link #isDataAvailable()} will return + * {@code false}. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@code ParseObject}. + */ + public ParseObject getParseObject(String key) { + Object value = get(key); + if (!(value instanceof ParseObject)) { + return null; + } + return (ParseObject) value; + } + + /** + * Access a {@link ParseUser} value. This function will not perform a network request. Unless the + * {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling + * {@link #fetchIfNeeded()} or {@link #refresh()}), {@link #isDataAvailable()} will return + * {@code false}. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if the value is not a {@link ParseUser}. + */ + public ParseUser getParseUser(String key) { + Object value = get(key); + if (!(value instanceof ParseUser)) { + return null; + } + return (ParseUser) value; + } + + /** + * Access a {@link ParseFile} value. This function will not perform a network request. Unless the + * {@link ParseFile} has been downloaded (e.g. by calling {@link ParseFile#getData()}), + * {@link ParseFile#isDataAvailable()} will return {@code false}. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key or if it is not a {@link ParseFile}. + */ + public ParseFile getParseFile(String key) { + Object value = get(key); + if (!(value instanceof ParseFile)) { + return null; + } + return (ParseFile) value; + } + + /** + * Access a {@link ParseGeoPoint} value. + * + * @param key + * The key to access the value for + * @return {@code null} if there is no such key or if it is not a {@link ParseGeoPoint}. + */ + public ParseGeoPoint getParseGeoPoint(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof ParseGeoPoint)) { + return null; + } + return (ParseGeoPoint) value; + } + } + + /** + * Access a {@link ParsePolygon} value. + * + * @param key + * The key to access the value for + * @return {@code null} if there is no such key or if it is not a {@link ParsePolygon}. + */ + public ParsePolygon getParsePolygon(String key) { + synchronized (mutex) { + checkGetAccess(key); + Object value = estimatedData.get(key); + if (!(value instanceof ParsePolygon)) { + return null; + } + return (ParsePolygon) value; + } + } + + /** + * Access the {@link ParseACL} governing this object. + */ + public ParseACL getACL() { + return getACL(true); + } + + private ParseACL getACL(boolean mayCopy) { + synchronized (mutex) { + checkGetAccess(KEY_ACL); + Object acl = estimatedData.get(KEY_ACL); + if (acl == null) { + return null; + } + if (!(acl instanceof ParseACL)) { + throw new RuntimeException("only ACLs can be stored in the ACL key"); + } + if (mayCopy && ((ParseACL) acl).isShared()) { + ParseACL copy = new ParseACL((ParseACL) acl); + estimatedData.put(KEY_ACL, copy); + return copy; + } + return (ParseACL) acl; + } + } + + /** + * Set the {@link ParseACL} governing this object. + */ + public void setACL(ParseACL acl) { + put(KEY_ACL, acl); + } + + /** + * Gets whether the {@code ParseObject} has been fetched. + * + * @return {@code true} if the {@code ParseObject} is new or has been fetched or refreshed. {@code false} + * otherwise. + */ + public boolean isDataAvailable() { + synchronized (mutex) { + return state.isComplete(); + } + } + + /** + * Gets whether the {@code ParseObject} specified key has been fetched. + * This means the property can be accessed safely. + * + * @return {@code true} if the {@code ParseObject} key is new or has been fetched or refreshed. {@code false} + * otherwise. + */ + public boolean isDataAvailable(String key) { + synchronized (mutex) { + // Fallback to estimatedData to include dirty changes. + return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key); + } + } + + /** + * Access or create a {@link ParseRelation} value for a key + * + * @param key + * The key to access the relation for. + * @return the ParseRelation object if the relation already exists for the key or can be created + * for this key. + */ + public ParseRelation getRelation(String key) { + synchronized (mutex) { + // All the sanity checking is done when add or remove is called on the relation. + Object value = estimatedData.get(key); + if (value instanceof ParseRelation) { + @SuppressWarnings("unchecked") + ParseRelation relation = (ParseRelation) value; + relation.ensureParentAndKey(this, key); + return relation; + } else { + ParseRelation relation = new ParseRelation<>(this, key); + /* + * We put the relation into the estimated data so that we'll get the same instance later, + * which may have known objects cached. If we rebuildEstimatedData, then this relation will + * be lost, and we'll get a new one. That's okay, because any cached objects it knows about + * must be replayable from the operations in the queue. If there were any objects in this + * relation that weren't still in the queue, then they would be in the copy of the + * ParseRelation that's in the serverData, so we would have gotten that instance instead. + */ + estimatedData.put(key, relation); + return relation; + } + } + } + + /** + * Access a value. In most cases it is more convenient to use a helper function such as + * {@link #getString(String)} or {@link #getInt(String)}. + * + * @param key + * The key to access the value for. + * @return {@code null} if there is no such key. + */ + public Object get(String key) { + synchronized (mutex) { + if (key.equals(KEY_ACL)) { + return getACL(); + } + + checkGetAccess(key); + Object value = estimatedData.get(key); + + // A relation may be deserialized without a parent or key. + // Either way, make sure it's consistent. + if (value instanceof ParseRelation) { + ((ParseRelation) value).ensureParentAndKey(this, key); + } + + return value; + } + } + + private void checkGetAccess(String key) { + if (!isDataAvailable(key)) { + throw new IllegalStateException( + "ParseObject has no data for '" + key + "'. Call fetchIfNeeded() to get the data."); + } + } + + public boolean hasSameId(ParseObject other) { + synchronized (mutex) { + return this.getClassName() != null && this.getObjectId() != null + && this.getClassName().equals(other.getClassName()) + && this.getObjectId().equals(other.getObjectId()); + } + } + + /* package */ void registerSaveListener(GetCallback callback) { + synchronized (mutex) { + saveEvent.subscribe(callback); + } + } + + /* package */ void unregisterSaveListener(GetCallback callback) { + synchronized (mutex) { + saveEvent.unsubscribe(callback); + } + } + + /** + * Called when a non-pointer is being created to allow additional initialization to occur. + */ + void setDefaultValues() { + if (needsDefaultACL() && ParseACL.getDefaultACL() != null) { + this.setACL(ParseACL.getDefaultACL()); + } + } + + /** + * Determines whether this object should get a default ACL. Override in subclasses to turn off + * default ACLs. + */ + boolean needsDefaultACL() { + return true; + } + + /** + * Registers the Parse-provided {@code ParseObject} subclasses. Do this here in a real method rather than + * as part of a static initializer because doing this in a static initializer can lead to + * deadlocks: https://our.intern.facebook.com/intern/tasks/?t=3508472 + */ + /* package */ static void registerParseSubclasses() { + registerSubclass(ParseUser.class); + registerSubclass(ParseRole.class); + registerSubclass(ParseInstallation.class); + registerSubclass(ParseSession.class); + + registerSubclass(ParsePin.class); + registerSubclass(EventuallyPin.class); + } + + /* package */ static void unregisterParseSubclasses() { + unregisterSubclass(ParseUser.class); + unregisterSubclass(ParseRole.class); + unregisterSubclass(ParseInstallation.class); + unregisterSubclass(ParseSession.class); + + unregisterSubclass(ParsePin.class); + unregisterSubclass(EventuallyPin.class); + } + + /** + * Default name for pinning if not specified. + * + * @see #pin() + * @see #unpin() + */ + public static final String DEFAULT_PIN = "_default"; + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAllInBackground(String, java.util.List, DeleteCallback) + * + * @param name + * the name + * @param objects + * the objects to be pinned + * @param callback + * the callback + */ + public static void pinAllInBackground(String name, + List objects, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(name, objects), callback); + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAllInBackground(String, java.util.List) + * + * @param name + * the name + * @param objects + * the objects to be pinned + * + * @return A {@link bolts.Task} that is resolved when pinning all completes. + */ + public static Task pinAllInBackground(final String name, + final List objects) { + return pinAllInBackground(name, objects, true); + } + + private static Task pinAllInBackground(final String name, + final List objects, final boolean includeAllChildren) { + if (!Parse.isLocalDatastoreEnabled()) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + + Task task = Task.forResult(null); + + // Resolve and persist unresolved users attached via ACL, similarly how we do in saveAsync + for (final ParseObject object : objects) { + task = task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!object.isDataAvailable(KEY_ACL)) { + return Task.forResult(null); + } + + final ParseACL acl = object.getACL(false); + if (acl == null) { + return Task.forResult(null); + } + + ParseUser user = acl.getUnresolvedUser(); + if (user == null || !user.isCurrentUser()) { + return Task.forResult(null); + } + + return ParseUser.pinCurrentUserIfNeededAsync(user); + } + }); + } + + return task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return Parse.getLocalDatastore().pinAllObjectsAsync( + name != null ? name : DEFAULT_PIN, + objects, + includeAllChildren); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // Hack to emulate persisting current user on disk after a save like in ParseUser#saveAsync + // Note: This does not persist current user if it's a child object of `objects`, it probably + // should, but we can't unless we do something similar to #deepSaveAsync. + if (ParseCorePlugins.PIN_CURRENT_USER.equals(name)) { + return task; + } + for (ParseObject object : objects) { + if (object instanceof ParseUser) { + final ParseUser user = (ParseUser) object; + if (user.isCurrentUser()) { + return ParseUser.pinCurrentUserIfNeededAsync(user); + } + } + } + return task; + } + }); + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAll(String, java.util.List) + * + * @param name + * the name + * @param objects + * the objects to be pinned + * + * @throws ParseException + */ + public static void pinAll(String name, + List objects) throws ParseException { + ParseTaskUtils.wait(pinAllInBackground(name, objects)); + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAllInBackground(java.util.List, DeleteCallback) + * @see #DEFAULT_PIN + * + * @param objects + * the objects to be pinned + * @param callback + * the callback + */ + public static void pinAllInBackground(List objects, + SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(DEFAULT_PIN, objects), callback); + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAllInBackground(java.util.List) + * @see #DEFAULT_PIN + * + * @param objects + * the objects to be pinned + * + * @return A {@link bolts.Task} that is resolved when pinning all completes. + */ + public static Task pinAllInBackground(List objects) { + return pinAllInBackground(DEFAULT_PIN, objects); + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. + * + * @see #unpinAll(java.util.List) + * @see #DEFAULT_PIN + * + * @param objects + * the objects to be pinned + * @throws ParseException + */ + public static void pinAll(List objects) throws ParseException { + ParseTaskUtils.wait(pinAllInBackground(DEFAULT_PIN, objects)); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(String, java.util.List, SaveCallback) + * + * @param name + * the name + * @param objects + * the objects + * @param callback + * the callback + */ + public static void unpinAllInBackground(String name, List objects, + DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name, objects), callback); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(String, java.util.List) + * + * @param name + * the name + * @param objects + * the objects + * + * @return A {@link bolts.Task} that is resolved when unpinning all completes. + */ + public static Task unpinAllInBackground(String name, + List objects) { + if (!Parse.isLocalDatastoreEnabled()) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + if (name == null) { + name = DEFAULT_PIN; + } + return Parse.getLocalDatastore().unpinAllObjectsAsync(name, objects); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(String, java.util.List) + * + * @param name + * the name + * @param objects + * the objects + * + * @throws ParseException + */ + public static void unpinAll(String name, + List objects) throws ParseException { + ParseTaskUtils.wait(unpinAllInBackground(name, objects)); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(java.util.List, SaveCallback) + * @see #DEFAULT_PIN + * + * @param objects + * the objects + * @param callback + * the callback + */ + public static void unpinAllInBackground(List objects, + DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(DEFAULT_PIN, objects), callback); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(java.util.List) + * @see #DEFAULT_PIN + * + * @param objects + * the objects + * + * @return A {@link bolts.Task} that is resolved when unpinning all completes. + */ + public static Task unpinAllInBackground(List objects) { + return unpinAllInBackground(DEFAULT_PIN, objects); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(java.util.List) + * @see #DEFAULT_PIN + * + * @param objects + * the objects + * + * @throws ParseException + */ + public static void unpinAll(List objects) throws ParseException { + ParseTaskUtils.wait(unpinAllInBackground(DEFAULT_PIN, objects)); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(String, java.util.List) + * + * @param name + * the name + * @param callback + * the callback + */ + public static void unpinAllInBackground(String name, DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name), callback); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(String, java.util.List) + * + * @param name + * the name + * + * @return A {@link bolts.Task} that is resolved when unpinning all completes. + */ + public static Task unpinAllInBackground(String name) { + if (!Parse.isLocalDatastoreEnabled()) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + if (name == null) { + name = DEFAULT_PIN; + } + return Parse.getLocalDatastore().unpinAllObjectsAsync(name); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(String, java.util.List) + * + * @param name + * the name + * + * @throws ParseException + */ + public static void unpinAll(String name) throws ParseException { + ParseTaskUtils.wait(unpinAllInBackground(name)); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(java.util.List, SaveCallback) + * @see #DEFAULT_PIN + * + * @param callback + * the callback + */ + public static void unpinAllInBackground(DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(), callback); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAllInBackground(java.util.List, SaveCallback) + * @see #DEFAULT_PIN + * + * @return A {@link bolts.Task} that is resolved when unpinning all completes. + */ + public static Task unpinAllInBackground() { + return unpinAllInBackground(DEFAULT_PIN); + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @see #pinAll(java.util.List) + * @see #DEFAULT_PIN + * + * @throws ParseException + */ + public static void unpinAll() throws ParseException { + ParseTaskUtils.wait(unpinAllInBackground()); + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with do + * nothing. + */ + @SuppressWarnings("unchecked") + /* package */ Task fetchFromLocalDatastoreAsync() { + if (!Parse.isLocalDatastoreEnabled()) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + return Parse.getLocalDatastore().fetchLocallyAsync((T) this); + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with do + * nothing. + */ + public void fetchFromLocalDatastoreInBackground(GetCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(this.fetchFromLocalDatastoreAsync(), callback); + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with throw a + * CACHE_MISS exception. + * + * @throws ParseException + */ + public void fetchFromLocalDatastore() throws ParseException { + ParseTaskUtils.wait(fetchFromLocalDatastoreAsync()); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @see #unpinInBackground(String, DeleteCallback) + * + * @param callback + * the callback + */ + public void pinInBackground(String name, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(name), callback); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @return A {@link bolts.Task} that is resolved when pinning completes. + * + * @see #unpinInBackground(String) + */ + public Task pinInBackground(String name) { + return pinAllInBackground(name, Collections.singletonList(this)); + } + + /* package */ Task pinInBackground(String name, boolean includeAllChildren) { + return pinAllInBackground(name, Collections.singletonList(this), includeAllChildren); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @see #unpin(String) + * + * @throws ParseException + */ + public void pin(String name) throws ParseException { + ParseTaskUtils.wait(pinInBackground(name)); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @see #unpinInBackground(DeleteCallback) + * @see #DEFAULT_PIN + * + * @param callback + * the callback + */ + public void pinInBackground(SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(), callback); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @return A {@link bolts.Task} that is resolved when pinning completes. + * + * @see #unpinInBackground() + * @see #DEFAULT_PIN + */ + public Task pinInBackground() { + return pinAllInBackground(DEFAULT_PIN, Arrays.asList(this)); + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with + * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on + * it. + * + * @see #unpin() + * @see #DEFAULT_PIN + * + * @throws ParseException + */ + public void pin() throws ParseException { + ParseTaskUtils.wait(pinInBackground()); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see #pinInBackground(String, SaveCallback) + * + * @param callback + * the callback + */ + public void unpinInBackground(String name, DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(name), callback); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @return A {@link bolts.Task} that is resolved when unpinning completes. + * + * @see #pinInBackground(String) + */ + public Task unpinInBackground(String name) { + return unpinAllInBackground(name, Arrays.asList(this)); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see #pin(String) + */ + public void unpin(String name) throws ParseException { + ParseTaskUtils.wait(unpinInBackground(name)); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see #pinInBackground(SaveCallback) + * @see #DEFAULT_PIN + * + * @param callback + * the callback + */ + public void unpinInBackground(DeleteCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(), callback); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @return A {@link bolts.Task} that is resolved when unpinning completes. + * + * @see #pinInBackground() + * @see #DEFAULT_PIN + */ + public Task unpinInBackground() { + return unpinAllInBackground(DEFAULT_PIN, Arrays.asList(this)); + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see #pin() + * @see #DEFAULT_PIN + */ + public void unpin() throws ParseException { + ParseTaskUtils.wait(unpinInBackground()); + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder(this)); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + synchronized (mutex) { + // Developer warnings. + ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled(); + boolean saving = hasOutstandingOperations(); + boolean deleting = isDeleting || isDeletingEventually > 0; + if (saving) { + Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will act as if these tasks have failed. This means that " + + "the subsequent call to save() will update again the same keys, and this is dangerous " + + "for certain operations, like increment(). To avoid inconsistencies, wait for operations " + + "to end before parceling."); + } + if (deleting) { + Log.w(TAG, "About to parcel a ParseObject while a delete / deleteEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will assume it's not deleted, and might incorrectly " + + "return false for isDirty(). To avoid inconsistencies, wait for operations to end " + + "before parceling."); + } + // Write className and id first, regardless of state. + dest.writeString(getClassName()); + String objectId = getObjectId(); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) dest.writeString(objectId); + // Write state and other members + state.writeToParcel(dest, encoder); + dest.writeByte(localId != null ? (byte) 1 : 0); + if (localId != null) dest.writeString(localId); + dest.writeByte(isDeleted ? (byte) 1 : 0); + // Care about dirty changes and ongoing tasks. + ParseOperationSet set; + if (hasOutstandingOperations()) { + // There's more than one set. Squash the queue, creating copies + // to preserve the original queue when LDS is enabled. + set = new ParseOperationSet(); + for (ParseOperationSet operationSet : operationSetQueue) { + ParseOperationSet copy = new ParseOperationSet(operationSet); + copy.mergeFrom(set); + set = copy; + } + } else { + set = operationSetQueue.getLast(); + } + set.setIsSaveEventually(false); + set.toParcel(dest, encoder); + // Pass a Bundle to subclasses. + Bundle bundle = new Bundle(); + onSaveInstanceState(bundle); + dest.writeBundle(bundle); + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseObject createFromParcel(Parcel source) { + return ParseObject.createFromParcel(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseObject[] newArray(int size) { + return new ParseObject[size]; + } + }; + + /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) { + String className = source.readString(); + String objectId = source.readByte() == 1 ? source.readString() : null; + // Create empty object (might be the same instance if LDS is enabled) + // and pass to decoder before unparceling child objects in State + ParseObject object = createWithoutData(className, objectId); + if (decoder instanceof ParseObjectParcelDecoder) { + ((ParseObjectParcelDecoder) decoder).addKnownObject(object); + } + State state = State.createFromParcel(source, decoder); + object.setState(state); + if (source.readByte() == 1) object.localId = source.readString(); + if (source.readByte() == 1) object.isDeleted = true; + // If object.ldsEnabledWhenParceling is true, we got this from OfflineStore. + // There is no need to restore operations in that case. + boolean restoreOperations = !object.ldsEnabledWhenParceling; + ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); + if (restoreOperations) { + for (String key : set.keySet()) { + ParseFieldOperation op = set.get(key); + object.performOperation(key, op); // Update ops and estimatedData + } + } + Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); + object.onRestoreInstanceState(bundle); + return object; + } + + /** + * Called when parceling this ParseObject. + * Subclasses can put values into the provided {@link Bundle} and receive them later + * {@link #onRestoreInstanceState(Bundle)}. Note that internal fields are already parceled by + * the framework. + * + * @param outState Bundle to host extra values + */ + protected void onSaveInstanceState(Bundle outState) {} + + /** + * Called when unparceling this ParseObject. + * Subclasses can read values from the provided {@link Bundle} that were previously put + * during {@link #onSaveInstanceState(Bundle)}. At this point the internal state is already + * recovered. + * + * @param savedState Bundle to read the values from + */ + protected void onRestoreInstanceState(Bundle savedState) {} + +} + +// [1] Normally we should only construct the command from state when it's our turn in the +// taskQueue so that new objects will have an updated objectId from previous saves. +// We can't do this for save/deleteEventually since this will break the promise that we'll +// try to run the command eventually, since our process might die before it's our turn in +// the taskQueue. +// This seems like this will only be a problem for new objects that are saved & +// save/deleteEventually'd at the same time, as the first will create 2 objects and the second +// the delete might fail. diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCoder.java new file mode 100644 index 0000000..498f4f7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCoder.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +/** + * Handles encoding/decoding ParseObjects to/from REST JSON. + */ +/** package */ class ParseObjectCoder { + + private static final String KEY_OBJECT_ID = "objectId"; + private static final String KEY_CLASS_NAME = "className"; + private static final String KEY_ACL = "ACL"; + private static final String KEY_CREATED_AT = "createdAt"; + private static final String KEY_UPDATED_AT = "updatedAt"; + + private static final ParseObjectCoder INSTANCE = new ParseObjectCoder(); + public static ParseObjectCoder get() { + return INSTANCE; + } + + /* package */ ParseObjectCoder() { + // do nothing + } + + /** + * Converts a {@code ParseObject.State} to REST JSON for saving. + * + * Only dirty keys from {@code operations} are represented in the data. Non-dirty keys such as + * {@code updatedAt}, {@code createdAt}, etc. are not included. + * + * @param state + * {@link ParseObject.State} of the type of {@link ParseObject} that will be returned. + * Properties are completely ignored. + * @param operations + * Dirty operations that are to be saved. + * @param encoder + * Encoder instance that will be used to encode the request. + * @return + * A REST formatted {@link JSONObject} that will be used for saving. + */ + public JSONObject encode( + T state, ParseOperationSet operations, ParseEncoder encoder) { + JSONObject objectJSON = new JSONObject(); + + try { + // Serialize the data + for (String key : operations.keySet()) { + ParseFieldOperation operation = operations.get(key); + objectJSON.put(key, encoder.encode(operation)); + + // TODO(grantland): Use cached value from hashedObjects if it's a set operation. + } + + if (state.objectId() != null) { + objectJSON.put(KEY_OBJECT_ID, state.objectId()); + } + } catch (JSONException e) { + throw new RuntimeException("could not serialize object to JSON"); + } + + return objectJSON; + } + + /** + * Converts REST JSON response to {@link ParseObject.State.Init}. + * + * This returns Builder instead of a State since we'll probably want to set some additional + * properties on it after decoding such as {@link ParseObject.State.Init#isComplete()}, etc. + * + * @param builder + * A {@link ParseObject.State.Init} instance that will have the server JSON applied + * (mutated) to it. This will generally be a instance created by clearing a mutable + * copy of a {@link ParseObject.State} to ensure it's an instance of the correct + * subclass: {@code state.newBuilder().clear()} + * @param json + * JSON response in REST format from the server. + * @param decoder + * Decoder instance that will be used to decode the server response. + * @return + * The same Builder instance passed in after the JSON is applied. + */ + public > T decode( + T builder, JSONObject json, ParseDecoder decoder) { + try { + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + /* + __type: Returned by queries and cloud functions to designate body is a ParseObject + __className: Used by fromJSON, should be stripped out by the time it gets here... + */ + if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) { + continue; + } + if (key.equals(KEY_OBJECT_ID)) { + String newObjectId = json.getString(key); + builder.objectId(newObjectId); + continue; + } + if (key.equals(KEY_CREATED_AT)) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + if (key.equals(KEY_UPDATED_AT)) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); + continue; + } + if (key.equals(KEY_ACL)) { + ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder); + builder.put(KEY_ACL, acl); + continue; + } + + Object value = json.get(key); + Object decodedObject = decoder.decode(value); + builder.put(key, decodedObject); + } + + return builder; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectController.java new file mode 100644 index 0000000..6d6a87e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectController.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +import java.util.List; + +import bolts.Task; + +/** package */ interface ParseObjectController { + + Task fetchAsync( + ParseObject.State state, String sessionToken, ParseDecoder decoder); + + Task saveAsync( + ParseObject.State state, + ParseOperationSet operations, + String sessionToken, + ParseDecoder decoder); + + List> saveAllAsync( + List states, + List operationsList, + String sessionToken, + List decoders); + + Task deleteAsync(ParseObject.State state, String sessionToken); + + List> deleteAllAsync(List states, String sessionToken); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentCoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentCoder.java new file mode 100644 index 0000000..4f18cf2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentCoder.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; +import java.util.Iterator; + +/** + * Handles encoding/decoding ParseObjects to/from /2 format JSON. /2 format json is only used for + * persisting current ParseObject(currentInstallation, currentUser) to disk when LDS is not enabled. + */ +/** package */ class ParseObjectCurrentCoder extends ParseObjectCoder { + + /* + /2 format JSON Keys + */ + private static final String KEY_OBJECT_ID = "objectId"; + private static final String KEY_CLASS_NAME = "classname"; + private static final String KEY_CREATED_AT = "createdAt"; + private static final String KEY_UPDATED_AT = "updatedAt"; + private static final String KEY_DATA = "data"; + + /* + Old serialized JSON keys + */ + private static final String KEY_OLD_OBJECT_ID = "id"; + private static final String KEY_OLD_CREATED_AT = "created_at"; + private static final String KEY_OLD_UPDATED_AT = "updated_at"; + private static final String KEY_OLD_POINTERS = "pointers"; + + private static final ParseObjectCurrentCoder INSTANCE = + new ParseObjectCurrentCoder(); + + public static ParseObjectCurrentCoder get() { + return INSTANCE; + } + + /* package */ ParseObjectCurrentCoder() { + // do nothing + } + + /** + * Converts a {@code ParseObject} to /2/ JSON representation suitable for saving to disk. + * + *
+   * {
+   *   data: {
+   *     // data fields, including objectId, createdAt, updatedAt
+   *   },
+   *   classname: class name for the object,
+   *   operations: { } // operations per field
+   * }
+   * 
+ * + * All keys are included, regardless of whether they are dirty. + * + * @see #decode(ParseObject.State.Init, JSONObject, ParseDecoder) + */ + @Override + public JSONObject encode( + T state, ParseOperationSet operations, ParseEncoder encoder) { + if (operations != null) { + throw new IllegalArgumentException("Parameter ParseOperationSet is not null"); + } + + // Public data goes in dataJSON; special fields go in objectJSON. + JSONObject objectJSON = new JSONObject(); + JSONObject dataJSON = new JSONObject(); + + try { + // Serialize the data + for (String key : state.keySet()) { + Object object = state.get(key); + dataJSON.put(key, encoder.encode(object)); + + // TODO(grantland): Use cached value from hashedObjects, but only if we're not dirty. + } + + if (state.createdAt() > 0) { + dataJSON.put(KEY_CREATED_AT, + ParseDateFormat.getInstance().format(new Date(state.createdAt()))); + } + if (state.updatedAt() > 0) { + dataJSON.put(KEY_UPDATED_AT, + ParseDateFormat.getInstance().format(new Date(state.updatedAt()))); + } + if (state.objectId() != null) { + dataJSON.put(KEY_OBJECT_ID, state.objectId()); + } + + objectJSON.put(KEY_DATA, dataJSON); + objectJSON.put(KEY_CLASS_NAME, state.className()); + } catch (JSONException e) { + throw new RuntimeException("could not serialize object to JSON"); + } + + return objectJSON; + } + + /** + * Decodes from /2/ JSON. + * + * This is only used to read ParseObjects stored on disk in JSON. + * + * @see #encode(ParseObject.State, ParseOperationSet, ParseEncoder) + */ + @Override + public > T decode( + T builder, JSONObject json, ParseDecoder decoder) { + try { + // The handlers for id, created_at, updated_at, and pointers are for + // backward compatibility with old serialized users. + if (json.has(KEY_OLD_OBJECT_ID)) { + String newObjectId = json.getString(KEY_OLD_OBJECT_ID); + builder.objectId(newObjectId); + } + if (json.has(KEY_OLD_CREATED_AT)) { + String createdAtString = + json.getString(KEY_OLD_CREATED_AT); + if (createdAtString != null) { + builder.createdAt(ParseImpreciseDateFormat.getInstance().parse(createdAtString)); + } + } + if (json.has(KEY_OLD_UPDATED_AT)) { + String updatedAtString = + json.getString(KEY_OLD_UPDATED_AT); + if (updatedAtString != null) { + builder.updatedAt(ParseImpreciseDateFormat.getInstance().parse(updatedAtString)); + } + } + if (json.has(KEY_OLD_POINTERS)) { + JSONObject newPointers = + json.getJSONObject(KEY_OLD_POINTERS); + Iterator keys = newPointers.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + JSONArray pointerArray = newPointers.getJSONArray(key); + builder.put(key, ParseObject.createWithoutData(pointerArray.optString(0), + pointerArray.optString(1))); + } + } + + JSONObject data = json.optJSONObject(KEY_DATA); + if (data != null) { + Iterator keys = data.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + + if (key.equals(KEY_OBJECT_ID)) { + String newObjectId = data.getString(key); + builder.objectId(newObjectId); + continue; + } + if (key.equals(KEY_CREATED_AT)) { + builder.createdAt(ParseDateFormat.getInstance().parse(data.getString(key))); + continue; + } + if (key.equals(KEY_UPDATED_AT)) { + builder.updatedAt(ParseDateFormat.getInstance().parse(data.getString(key))); + continue; + } + + Object value = data.get(key); + Object decodedObject = decoder.decode(value); + builder.put(key, decodedObject); + } + } + + return builder; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentController.java new file mode 100644 index 0000000..47aa55d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectCurrentController.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import bolts.Task; + +/** package */ interface ParseObjectCurrentController { + + /** + * Persist the currentParseObject + * @param object + * @return + */ + Task setAsync(T object); + + /** + * Get the persisted currentParseObject + * @return + */ + Task getAsync(); + + /** + * Check whether the currentParseObject exists or not + * @return + */ + Task existsAsync(); + + /** + * Judge whether the given ParseObject is the currentParseObject + * @param object + * @return {@code true} if the give {@link ParseObject} is the currentParseObject + */ + boolean isCurrent(T object); + + /** + * A test helper to reset the current ParseObject. This method nullifies the in memory + * currentParseObject + */ + void clearFromMemory(); + + /** + * A test helper to reset the current ParseObject. This method nullifies the in memory and in + * disk currentParseObject + */ + void clearFromDisk(); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java new file mode 100644 index 0000000..772c47c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java @@ -0,0 +1,43 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelDecoder} that remembers which + * {@code ParseObject}s have been decoded. When a pointer is found and we have already decoded + * an instance for the same object id, we use the decoded instance. + * + * This is very similar to what {@link KnownParseObjectDecoder} does for JSON. + */ +/* package */ class ParseObjectParcelDecoder extends ParseParcelDecoder { + + private Map objects = new HashMap<>(); + + public ParseObjectParcelDecoder() {} + + public void addKnownObject(ParseObject object) { + objects.put(getObjectOrLocalId(object), object); + } + + @Override + protected ParseObject decodePointer(Parcel source) { + String className = source.readString(); + String objectId = source.readString(); + if (objects.containsKey(objectId)) { + return objects.get(objectId); + } + // Should not happen if encoding was done through ParseObjectParcelEncoder. + ParseObject object = ParseObject.createWithoutData(className, objectId); + objects.put(objectId, object); + return object; + } + + private String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java new file mode 100644 index 0000000..399e568 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java @@ -0,0 +1,38 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashSet; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelEncoder} that remembers which + * {@code ParseObject}s have been encoded. If an object is found again in the object tree, + * it is encoded as a pointer rather than a full object, to avoid {@code StackOverflowError}s + * due to circular references. + */ +/* package */ class ParseObjectParcelEncoder extends ParseParcelEncoder { + + private Set ids = new HashSet<>(); + + public ParseObjectParcelEncoder() {} + + public ParseObjectParcelEncoder(ParseObject root) { + ids.add(getObjectOrLocalId(root)); + } + + @Override + protected void encodeParseObject(ParseObject object, Parcel dest) { + String id = getObjectOrLocalId(object); + if (ids.contains(id)) { + encodePointer(object.getClassName(), id, dest); + } else { + ids.add(id); + super.encodeParseObject(object, dest); + } + } + + private String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectStore.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectStore.java new file mode 100644 index 0000000..d5e75b0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectStore.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import bolts.Task; + +/** package */ interface ParseObjectStore { + + Task getAsync(); + + Task setAsync(T object); + + Task existsAsync(); + + Task deleteAsync(); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java new file mode 100644 index 0000000..a5bd92f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseObjectSubclassingController.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +/* package */ class ParseObjectSubclassingController { + private final Object mutex = new Object(); + private final Map> registeredSubclasses = new HashMap<>(); + + /* package */ String getClassName(Class clazz) { + ParseClassName info = clazz.getAnnotation(ParseClassName.class); + if (info == null) { + throw new IllegalArgumentException("No ParseClassName annotation provided on " + clazz); + } + return info.value(); + } + + /* package */ boolean isSubclassValid(String className, Class clazz) { + Constructor constructor = null; + + synchronized (mutex) { + constructor = registeredSubclasses.get(className); + } + + return constructor == null + ? clazz == ParseObject.class + : constructor.getDeclaringClass() == clazz; + } + + /* package */ void registerSubclass(Class clazz) { + if (!ParseObject.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("Cannot register a type that is not a subclass of ParseObject"); + } + + String className = getClassName(clazz); + Constructor previousConstructor = null; + + synchronized (mutex) { + previousConstructor = registeredSubclasses.get(className); + if (previousConstructor != null) { + Class previousClass = previousConstructor.getDeclaringClass(); + if (clazz.isAssignableFrom(previousClass)) { + // Previous subclass is more specific or equal to the current type, do nothing. + return; + } else if (previousClass.isAssignableFrom(clazz)) { + // Previous subclass is parent of new child subclass, fallthrough and actually + // register this class. + /* Do nothing */ + } else { + throw new IllegalArgumentException( + "Tried to register both " + previousClass.getName() + " and " + clazz.getName() + + " as the ParseObject subclass of " + className + ". " + "Cannot determine the right " + + "class to use because neither inherits from the other." + ); + } + } + + try { + registeredSubclasses.put(className, getConstructor(clazz)); + } catch (NoSuchMethodException ex) { + throw new IllegalArgumentException( + "Cannot register a type that does not implement the default constructor!" + ); + } catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Default constructor for " + clazz + " is not accessible." + ); + } + } + + if (previousConstructor != null) { + // TODO: This is super tightly coupled. Let's remove it when automatic registration is in. + // NOTE: Perform this outside of the mutex, to prevent any potential deadlocks. + if (className.equals(getClassName(ParseUser.class))) { + ParseUser.getCurrentUserController().clearFromMemory(); + } else if (className.equals(getClassName(ParseInstallation.class))) { + ParseInstallation.getCurrentInstallationController().clearFromMemory(); + } + } + } + + /* package */ void unregisterSubclass(Class clazz) { + String className = getClassName(clazz); + + synchronized (mutex) { + registeredSubclasses.remove(className); + } + } + + /* package */ ParseObject newInstance(String className) { + Constructor constructor = null; + + synchronized (mutex) { + constructor = registeredSubclasses.get(className); + } + + try { + return constructor != null + ? constructor.newInstance() + : new ParseObject(className); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Failed to create instance of subclass.", e); + } + } + + private static Constructor getConstructor(Class clazz) throws NoSuchMethodException, IllegalAccessException { + Constructor constructor = clazz.getDeclaredConstructor(); + if (constructor == null) { + throw new NoSuchMethodException(); + } + int modifiers = constructor.getModifiers(); + if (Modifier.isPublic(modifiers) || (clazz.getPackage().getName().equals("com.parse") && + !(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers)))) { + return constructor; + } + throw new IllegalAccessException(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseOperationSet.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseOperationSet.java new file mode 100644 index 0000000..1cddc29 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseOperationSet.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.UUID; + +/** + * A set of field-level operations that can be performed on an object, corresponding to one command. + * For example, all of the data for a single call to save() will be packaged here. It is assumed + * that the ParseObject that owns the operations handles thread-safety. + */ +/** package */ class ParseOperationSet extends HashMap { + private static final long serialVersionUID = 1L; + + private static final String REST_KEY_IS_SAVE_EVENTUALLY = "__isSaveEventually"; + private static final String REST_KEY_UUID = "__uuid"; + + // A unique id for this operation set. + private final String uuid; + + // Does this set correspond to a call to saveEventually? + private boolean isSaveEventually = false; + + /** + * Creates a new operation set with a random UUID. + */ + public ParseOperationSet() { + this(UUID.randomUUID().toString()); + } + + public ParseOperationSet(ParseOperationSet operations) { + super(operations); + uuid = operations.getUUID(); + isSaveEventually = operations.isSaveEventually; + } + + /** + * Creates a new operation set with the given UUID. + */ + private ParseOperationSet(String uuid) { + this.uuid = uuid; + } + + public String getUUID() { + return uuid; + } + + public void setIsSaveEventually(boolean value) { + isSaveEventually = value; + } + + public boolean isSaveEventually() { + return isSaveEventually; + } + + /** + * Merges the changes from the given operation set into this one. Most typically, this is what + * happens when a save fails and changes need to be rolled into the next save. + */ + public void mergeFrom(ParseOperationSet other) { + for (String key : other.keySet()) { + ParseFieldOperation operation1 = other.get(key); + ParseFieldOperation operation2 = get(key); + if (operation2 != null) { + operation2 = operation2.mergeWithPrevious(operation1); + } else { + operation2 = operation1; + } + put(key, operation2); + } + } + + /** + * Converts this operation set into its REST format for serializing to LDS. + */ + public JSONObject toRest(ParseEncoder objectEncoder) throws JSONException { + JSONObject operationSetJSON = new JSONObject(); + for (String key : keySet()) { + ParseFieldOperation op = get(key); + operationSetJSON.put(key, op.encode(objectEncoder)); + } + + operationSetJSON.put(REST_KEY_UUID, uuid); + if (isSaveEventually) { + operationSetJSON.put(REST_KEY_IS_SAVE_EVENTUALLY, true); + } + return operationSetJSON; + } + + /** + * The inverse of toRest. Creates a new OperationSet from the given JSON. + */ + public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) + throws JSONException { + // Copy the json object to avoid making changes to the old object + Iterator keysIter = json.keys(); + String[] keys = new String[json.length()]; + int index = 0; + while (keysIter.hasNext()) { + String key = keysIter.next(); + keys[index++] = key; + } + + JSONObject jsonCopy = new JSONObject(json, keys); + String uuid = (String) jsonCopy.remove(REST_KEY_UUID); + ParseOperationSet operationSet = + (uuid == null ? new ParseOperationSet() : new ParseOperationSet(uuid)); + + boolean isSaveEventually = jsonCopy.optBoolean(REST_KEY_IS_SAVE_EVENTUALLY); + jsonCopy.remove(REST_KEY_IS_SAVE_EVENTUALLY); + operationSet.setIsSaveEventually(isSaveEventually); + + Iterator opKeys = jsonCopy.keys(); + while (opKeys.hasNext()) { + String opKey = (String) opKeys.next(); + Object value = decoder.decode(jsonCopy.get(opKey)); + ParseFieldOperation fieldOp; + if (opKey.equals("ACL")) { + value = ParseACL.createACLFromJSONObject(jsonCopy.getJSONObject(opKey), decoder); + } + if (value instanceof ParseFieldOperation) { + fieldOp = (ParseFieldOperation) value; + } else { + fieldOp = new ParseSetOperation(value); + } + operationSet.put(opKey, fieldOp); + } + + return operationSet; + } + + /** + * Parcels this operation set into a Parcel with the given encoder. + */ + /* package */ void toParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeString(uuid); + dest.writeByte(isSaveEventually ? (byte) 1 : 0); + dest.writeInt(size()); + for (String key : keySet()) { + dest.writeString(key); + encoder.encode(get(key), dest); + } + } + + /* package */ static ParseOperationSet fromParcel(Parcel source, ParseParcelDecoder decoder) { + ParseOperationSet set = new ParseOperationSet(source.readString()); + set.setIsSaveEventually(source.readByte() == 1); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + ParseFieldOperation op = (ParseFieldOperation) decoder.decode(source); + set.put(key, op); + } + return set; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelDecoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelDecoder.java new file mode 100644 index 0000000..9f434e7 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelDecoder.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A {@code ParseParcelableDecoder} can be used to unparcel objects such as + * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. + * + * This is capable of decoding objects and pointers to them. + * However, for improved behavior in the case of {@link ParseObject}s, use the stateful + * implementation {@link ParseObjectParcelDecoder}. + * + * @see ParseParcelEncoder + * @see ParseObjectParcelDecoder + */ +/* package */ class ParseParcelDecoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelDecoder INSTANCE = new ParseParcelDecoder(); + public static ParseParcelDecoder get() { + return INSTANCE; + } + + public Object decode(Parcel source) { + String type = source.readString(); + switch (type) { + + case ParseParcelEncoder.TYPE_OBJECT: + return decodeParseObject(source); + + case ParseParcelEncoder.TYPE_POINTER: + return decodePointer(source); + + case ParseParcelEncoder.TYPE_DATE: + String iso = source.readString(); + return ParseDateFormat.getInstance().parse(iso); + + case ParseParcelEncoder.TYPE_BYTES: + byte[] bytes = new byte[source.readInt()]; + source.readByteArray(bytes); + return bytes; + + case ParseParcelEncoder.TYPE_OP: + return ParseFieldOperations.decode(source, this); + + case ParseParcelEncoder.TYPE_FILE: + return new ParseFile(source, this); + + case ParseParcelEncoder.TYPE_GEOPOINT: + return new ParseGeoPoint(source, this); + + case ParseParcelEncoder.TYPE_POLYGON: + return new ParsePolygon(source, this); + + case ParseParcelEncoder.TYPE_ACL: + return new ParseACL(source, this); + + case ParseParcelEncoder.TYPE_RELATION: + return new ParseRelation<>(source, this); + + case ParseParcelEncoder.TYPE_MAP: + int size = source.readInt(); + Map map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(source.readString(), decode(source)); + } + return map; + + case ParseParcelEncoder.TYPE_COLLECTION: + int length = source.readInt(); + List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + list.add(i, decode(source)); + } + return list; + + case ParseParcelEncoder.TYPE_JSON_NULL: + return JSONObject.NULL; + + case ParseParcelEncoder.TYPE_NULL: + return null; + + case ParseParcelEncoder.TYPE_NATIVE: + return source.readValue(null); // No need for a class loader. + + default: + throw new RuntimeException("Could not unparcel objects from this Parcel."); + + } + } + + protected ParseObject decodeParseObject(Parcel source) { + return ParseObject.createFromParcel(source, this); + } + + protected ParseObject decodePointer(Parcel source) { + // By default, use createWithoutData. Overriden in subclass. + return ParseObject.createWithoutData(source.readString(), source.readString()); + } + +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelEncoder.java new file mode 100644 index 0000000..2cf9054 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONObject; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A {@code ParseParcelableEncoder} can be used to parcel objects into a {@link android.os.Parcel}. + * + * This is capable of parceling {@link ParseObject}s, but the result can likely be a + * {@link StackOverflowError} due to circular references in the objects tree. + * When needing to parcel {@link ParseObject}, use the stateful {@link ParseObjectParcelEncoder}. + * + * @see ParseParcelDecoder + * @see ParseObjectParcelEncoder + */ +/* package */ class ParseParcelEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelEncoder INSTANCE = new ParseParcelEncoder(); + public static ParseParcelEncoder get() { + return INSTANCE; + } + + private static boolean isValidType(Object value) { + // This encodes to parcel what ParseEncoder does for JSON + return ParseEncoder.isValidType(value); + } + + /* package */ final static String TYPE_OBJECT = "Object"; + /* package */ final static String TYPE_POINTER = "Pointer"; + /* package */ final static String TYPE_DATE = "Date"; + /* package */ final static String TYPE_BYTES = "Bytes"; + /* package */ final static String TYPE_ACL = "Acl"; + /* package */ final static String TYPE_RELATION = "Relation"; + /* package */ final static String TYPE_MAP = "Map"; + /* package */ final static String TYPE_COLLECTION = "Collection"; + /* package */ final static String TYPE_JSON_NULL = "JsonNull"; + /* package */ final static String TYPE_NULL = "Null"; + /* package */ final static String TYPE_NATIVE = "Native"; + /* package */ final static String TYPE_OP = "Operation"; + /* package */ final static String TYPE_FILE = "File"; + /* package */ final static String TYPE_GEOPOINT = "GeoPoint"; + /* package */ final static String TYPE_POLYGON = "Polygon"; + + public void encode(Object object, Parcel dest) { + try { + if (object instanceof ParseObject) { + // By default, encode as a full ParseObject. Overriden in sublasses. + encodeParseObject((ParseObject) object, dest); + + } else if (object instanceof Date) { + dest.writeString(TYPE_DATE); + dest.writeString(ParseDateFormat.getInstance().format((Date) object)); + + } else if (object instanceof byte[]) { + dest.writeString(TYPE_BYTES); + byte[] bytes = (byte[]) object; + dest.writeInt(bytes.length); + dest.writeByteArray(bytes); + + } else if (object instanceof ParseFieldOperation) { + dest.writeString(TYPE_OP); + ((ParseFieldOperation) object).encode(dest, this); + + } else if (object instanceof ParseFile) { + dest.writeString(TYPE_FILE); + ((ParseFile) object).writeToParcel(dest, this); + + } else if (object instanceof ParseGeoPoint) { + dest.writeString(TYPE_GEOPOINT); + ((ParseGeoPoint) object).writeToParcel(dest, this); + + } else if (object instanceof ParsePolygon) { + dest.writeString(TYPE_POLYGON); + ((ParsePolygon) object).writeToParcel(dest, this); + + } else if (object instanceof ParseACL) { + dest.writeString(TYPE_ACL); + ((ParseACL) object).writeToParcel(dest, this); + + } else if (object instanceof ParseRelation) { + dest.writeString(TYPE_RELATION); + ((ParseRelation) object).writeToParcel(dest, this); + + } else if (object instanceof Map) { + dest.writeString(TYPE_MAP); + @SuppressWarnings("unchecked") + Map map = (Map) object; + dest.writeInt(map.size()); + for (Map.Entry pair : map.entrySet()) { + dest.writeString(pair.getKey()); + encode(pair.getValue(), dest); + } + + } else if (object instanceof Collection) { + dest.writeString(TYPE_COLLECTION); + Collection collection = (Collection) object; + dest.writeInt(collection.size()); + for (Object item : collection) { + encode(item, dest); + } + + } else if (object == JSONObject.NULL) { + dest.writeString(TYPE_JSON_NULL); + + } else if (object == null) { + dest.writeString(TYPE_NULL); + + // String, Number, Boolean. Simply use writeValue + } else if (isValidType(object)) { + dest.writeString(TYPE_NATIVE); + dest.writeValue(object); + + } else { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + + } catch (Exception e) { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + } + + protected void encodeParseObject(ParseObject object, Parcel dest) { + dest.writeString(TYPE_OBJECT); + object.writeToParcel(dest, this); + } + + protected void encodePointer(String className, String objectId, Parcel dest) { + dest.writeString(TYPE_POINTER); + dest.writeString(className); + dest.writeString(objectId); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePin.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePin.java new file mode 100644 index 0000000..ce75685 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePin.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +@ParseClassName("_Pin") +/** package */ class ParsePin extends ParseObject { + + /* package */ static final String KEY_NAME = "_name"; + private static final String KEY_OBJECTS = "_objects"; + + public ParsePin() { + // do nothing + } + + @Override + boolean needsDefaultACL() { + return false; + } + + public String getName() { + return getString(KEY_NAME); + } + + public void setName(String name) { + put(KEY_NAME, name); + } + + public List getObjects() { + return getList(KEY_OBJECTS); + } + + public void setObjects(List objects) { + put(KEY_OBJECTS, objects); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java new file mode 100644 index 0000000..0b2e86c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java @@ -0,0 +1,638 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * Manages all *Eventually calls when the local datastore is enabled. + * + * Constraints: + * - *Eventually calls must be executed in the same order they were queued. + * - *Eventually calls must only be executed when it's ParseOperationSet is ready in + * {@link ParseObject#taskQueue}. + * - All rules apply on start from reboot. + */ +/** package */ class ParsePinningEventuallyQueue extends ParseEventuallyQueue { + private static final String TAG = "ParsePinningEventuallyQueue"; + + /** + * TCS that is held until a {@link ParseOperationSet} is completed. + */ + private HashMap> pendingOperationSetUUIDTasks = + new HashMap<>(); + + /** + * Queue for reading/writing eventually operations. Makes all reads/writes atomic operations. + */ + private TaskQueue taskQueue = new TaskQueue(); + + /** + * Queue for running *Eventually operations. It uses waitForOperationSetAndEventuallyPin to + * synchronize {@link ParseObject#taskQueue} until they are both ready to process the same + * ParseOperationSet. + */ + private TaskQueue operationSetTaskQueue = new TaskQueue(); + + /** + * List of {@link ParseOperationSet#uuid} that are currently queued in + * {@link ParsePinningEventuallyQueue#operationSetTaskQueue}. + */ + private ArrayList eventuallyPinUUIDQueue = new ArrayList<>(); + + /** + * TCS that is created when there is no internet connection and isn't resolved until connectivity + * is achieved. + * + * If an error is set, it means that we are trying to clear out the taskQueues. + */ + private TaskCompletionSource connectionTaskCompletionSource = new TaskCompletionSource<>(); + private final Object connectionLock = new Object(); + private final ParseHttpClient httpClient; + + private ConnectivityNotifier notifier; + private ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() { + @Override + public void networkConnectivityStatusChanged(Context context, Intent intent) { + boolean connectionLost = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + if (connectionLost) { + setConnected(false); + } else { + setConnected(ConnectivityNotifier.isConnected(context)); + } + } + }; + + public ParsePinningEventuallyQueue(Context context, ParseHttpClient client) { + setConnected(ConnectivityNotifier.isConnected(context)); + + httpClient = client; + + notifier = ConnectivityNotifier.getNotifier(context); + notifier.addListener(listener); + + resume(); + } + + @Override + public void onDestroy() { + //TODO (grantland): pause #6484855 + + notifier.removeListener(listener); + } + + @Override + public void setConnected(boolean connected) { + synchronized (connectionLock) { + if (isConnected() != connected) { + super.setConnected(connected); + if (connected) { + connectionTaskCompletionSource.trySetResult(null); + connectionTaskCompletionSource = Task.create(); + connectionTaskCompletionSource.trySetResult(null); + } else { + connectionTaskCompletionSource = Task.create(); + } + } + } + } + + @Override + public int pendingCount() { + try { + return ParseTaskUtils.wait(pendingCountAsync()); + } catch (ParseException e) { + throw new IllegalStateException(e); + } + } + + public Task pendingCountAsync() { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + + taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return pendingCountAsync(toAwait).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + int count = task.getResult(); + tcs.setResult(count); + return Task.forResult(null); + } + }); + } + }); + + return tcs.getTask(); + } + + public Task pendingCountAsync(Task toAwait) { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return EventuallyPin.findAllPinned().continueWithTask(new Continuation, Task>() { + @Override + public Task then(Task> task) throws Exception { + List pins = task.getResult(); + return Task.forResult(pins.size()); + } + }); + } + }); + } + + @Override + public void pause() { + synchronized (connectionLock) { + // Error out tasks waiting on waitForConnectionAsync. + connectionTaskCompletionSource.trySetError(new PauseException()); + connectionTaskCompletionSource = Task.create(); + connectionTaskCompletionSource.trySetError(new PauseException()); + } + + synchronized (taskQueueSyncLock) { + for (String key : pendingEventuallyTasks.keySet()) { + // Error out tasks waiting on waitForOperationSetAndEventuallyPin. + pendingEventuallyTasks.get(key).trySetError(new PauseException()); + } + pendingEventuallyTasks.clear(); + uuidToOperationSet.clear(); + uuidToEventuallyPin.clear(); + } + + try { + ParseTaskUtils.wait(whenAll(Arrays.asList(taskQueue, operationSetTaskQueue))); + } catch (ParseException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void resume() { + // Reset waitForConnectionAsync. + if (isConnected()) { + connectionTaskCompletionSource.trySetResult(null); + connectionTaskCompletionSource = Task.create(); + connectionTaskCompletionSource.trySetResult(null); + } else { + connectionTaskCompletionSource = Task.create(); + } + + populateQueueAsync(); + } + + private Task waitForConnectionAsync() { + synchronized (connectionLock) { + return connectionTaskCompletionSource.getTask(); + } + } + + /** + * Pins the eventually operation on {@link ParsePinningEventuallyQueue#taskQueue}. + * + * @return Returns a Task that will be resolved when the command completes. + */ + @Override + public Task enqueueEventuallyAsync(final ParseRESTCommand command, + final ParseObject object) { + Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE); + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + + taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return enqueueEventuallyAsync(command, object, toAwait, tcs); + } + }); + + return tcs.getTask(); + } + + private Task enqueueEventuallyAsync(final ParseRESTCommand command, + final ParseObject object, Task toAwait, final TaskCompletionSource tcs) { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + Task pinTask = EventuallyPin.pinEventuallyCommand(object, command); + + return pinTask.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + EventuallyPin pin = task.getResult(); + Exception error = task.getError(); + if (error != null) { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + PLog.w(TAG, "Unable to save command for later.", error); + } + notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); + return Task.forResult(null); + } + + pendingOperationSetUUIDTasks.put(pin.getUUID(), tcs); + + // We don't need to wait for this. + populateQueueAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + /* + * We need to wait until after we populated the operationSetTaskQueue to notify + * that we've enqueued this command. + */ + notifyTestHelper(TestHelper.COMMAND_ENQUEUED); + return task; + } + }); + + return task.makeVoid(); + } + }); + } + }); + } + + /** + * Queries for pinned eventually operations on {@link ParsePinningEventuallyQueue#taskQueue}. + * + * @return Returns a Task that is resolved when all EventuallyPins are enqueued in the + * operationSetTaskQueue. + */ + private Task populateQueueAsync() { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return populateQueueAsync(toAwait); + } + }); + } + + private Task populateQueueAsync(Task toAwait) { + return toAwait.continueWithTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + // We don't want to enqueue any EventuallyPins that are already queued. + return EventuallyPin.findAllPinned(eventuallyPinUUIDQueue); + } + }).onSuccessTask(new Continuation, Task>() { + @Override + public Task then(Task> task) throws Exception { + List pins = task.getResult(); + + for (final EventuallyPin pin : pins) { + // We don't need to wait for this. + runEventuallyAsync(pin); + } + + return task.makeVoid(); + } + }); + } + + /** + * Queues an eventually operation on {@link ParsePinningEventuallyQueue#operationSetTaskQueue}. + * + * Each eventually operation is run synchronously to maintain the order in which they were + * enqueued. + */ + private Task runEventuallyAsync(final EventuallyPin eventuallyPin) { + final String uuid = eventuallyPin.getUUID(); + if (eventuallyPinUUIDQueue.contains(uuid)) { + // We don't want to enqueue the same operation more than once. + return Task.forResult(null); + } + eventuallyPinUUIDQueue.add(uuid); + + operationSetTaskQueue.enqueue(new Continuation>() { + @Override + public Task then(final Task toAwait) throws Exception { + return runEventuallyAsync(eventuallyPin, toAwait).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + eventuallyPinUUIDQueue.remove(uuid); + return task; + } + }); + } + }); + + return Task.forResult(null); + } + + /** + * Runs the eventually operation. It first waits for a valid connection and if it's a save, it + * also waits for the ParseObject to be ready. + * + * @return A task that is resolved when the eventually operation completes. + */ + private Task runEventuallyAsync(final EventuallyPin eventuallyPin, final Task toAwait) { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return waitForConnectionAsync(); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return waitForOperationSetAndEventuallyPin(null, eventuallyPin).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + Exception error = task.getError(); + if (error != null) { + if (error instanceof PauseException) { + // Bubble up the PauseException. + return task.makeVoid(); + } + + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + PLog.e(TAG, "Failed to run command.", error); + } + + notifyTestHelper(TestHelper.COMMAND_FAILED, error); + } else { + notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); + } + + TaskCompletionSource tcs = + pendingOperationSetUUIDTasks.remove(eventuallyPin.getUUID()); + if (tcs != null) { + if (error != null) { + tcs.setError(error); + } else { + tcs.setResult(task.getResult()); + } + } + return task.makeVoid(); + } + }); + } + }); + } + + /** + * Lock to make sure all changes to the below parameters happen atomically. + */ + private final Object taskQueueSyncLock = new Object(); + + /** + * Map of eventually operation UUID to TCS that is resolved when the operation is complete. + */ + private HashMap> pendingEventuallyTasks = + new HashMap<>(); + + /** + * Map of eventually operation UUID to matching ParseOperationSet. + */ + private HashMap uuidToOperationSet = new HashMap<>(); + + /** + * Map of eventually operation UUID to matching EventuallyPin. + */ + private HashMap uuidToEventuallyPin = new HashMap<>(); + + /** + * Synchronizes ParseObject#taskQueue (Many) and ParseCommandCache#taskQueue (One). Each queue + * will be held until both are ready, matched on operationSetUUID. Once both are ready, the + * eventually task will be run. + * + * @param operationSet + * From {@link ParseObject} + * @param eventuallyPin + * From {@link ParsePinningEventuallyQueue} + */ + //TODO (grantland): We can probably generalize this to synchronize/join more than 2 taskQueues + @Override + /* package */ Task waitForOperationSetAndEventuallyPin(ParseOperationSet operationSet, + EventuallyPin eventuallyPin) { + if (eventuallyPin != null && eventuallyPin.getType() != EventuallyPin.TYPE_SAVE) { + return process(eventuallyPin, null); + } + + final String uuid; // The key we use to join the taskQueues + final TaskCompletionSource tcs; + + synchronized (taskQueueSyncLock) { + if (operationSet != null && eventuallyPin == null) { + uuid = operationSet.getUUID(); + uuidToOperationSet.put(uuid, operationSet); + } else if (operationSet == null && eventuallyPin != null) { + uuid = eventuallyPin.getOperationSetUUID(); + uuidToEventuallyPin.put(uuid, eventuallyPin); + } else { + throw new IllegalStateException("Either operationSet or eventuallyPin must be set."); + } + + eventuallyPin = uuidToEventuallyPin.get(uuid); + operationSet = uuidToOperationSet.get(uuid); + + if (eventuallyPin == null || operationSet == null) { + if (pendingEventuallyTasks.containsKey(uuid)) { + tcs = pendingEventuallyTasks.get(uuid); + } else { + tcs = Task.create(); + pendingEventuallyTasks.put(uuid, tcs); + } + return tcs.getTask(); + } else { + tcs = pendingEventuallyTasks.get(uuid); + } + } + + return process(eventuallyPin, operationSet).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (taskQueueSyncLock) { + pendingEventuallyTasks.remove(uuid); + uuidToOperationSet.remove(uuid); + uuidToEventuallyPin.remove(uuid); + } + + Exception error = task.getError(); + if (error != null) { + tcs.trySetError(error); + } else if (task.isCancelled()) { + tcs.trySetCancelled(); + } else { + tcs.trySetResult(task.getResult()); + } + return tcs.getTask(); + } + }); + } + + /** + * Invokes the eventually operation. + */ + private Task process(final EventuallyPin eventuallyPin, + final ParseOperationSet operationSet) { + + return waitForConnectionAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final int type = eventuallyPin.getType(); + final ParseObject object = eventuallyPin.getObject(); + String sessionToken = eventuallyPin.getSessionToken(); + + Task executeTask; + if (type == EventuallyPin.TYPE_SAVE) { + executeTask = object.saveAsync(httpClient, operationSet, sessionToken); + } else if (type == EventuallyPin.TYPE_DELETE) { + executeTask = object.deleteAsync(sessionToken).cast(); + } else { // else if (type == EventuallyPin.TYPE_COMMAND) { + ParseRESTCommand command = eventuallyPin.getCommand(); + if (command == null) { + executeTask = Task.forResult(null); + notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED); + } else { + executeTask = command.executeAsync(httpClient); + } + } + + return executeTask.continueWithTask(new Continuation>() { + @Override + public Task then(final Task executeTask) throws Exception { + Exception error = executeTask.getError(); + if (error != null) { + if (error instanceof ParseException + && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { + // We did our retry logic in ParseRequest, so just mark as not connected + // and move on. + setConnected(false); + + notifyTestHelper(TestHelper.NETWORK_DOWN); + + return process(eventuallyPin, operationSet); + } + } + + // Delete the command regardless, even if it failed. Otherwise, we'll just keep + // trying it forever. + // We don't have to wait for taskQueue since it will not be enqueued again + // since this EventuallyPin is still in eventuallyPinUUIDQueue. + return eventuallyPin.unpinInBackground(EventuallyPin.PIN_NAME).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + JSONObject result = executeTask.getResult(); + if (type == EventuallyPin.TYPE_SAVE) { + return object.handleSaveEventuallyResultAsync(result, operationSet); + } else if (type == EventuallyPin.TYPE_DELETE) { + if (executeTask.isFaulted()) { + return task; + } else { + return object.handleDeleteEventuallyResultAsync(); + } + } else { // else if (type == EventuallyPin.TYPE_COMMAND) { + return task; + } + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return executeTask; + } + }); + } + }); + } + }); + } + + @Override + /* package */ void simulateReboot() { + pause(); + + pendingOperationSetUUIDTasks.clear(); + pendingEventuallyTasks.clear(); + uuidToOperationSet.clear(); + uuidToEventuallyPin.clear(); + + resume(); + } + + @Override + public void clear() { + pause(); + + Task task = taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return EventuallyPin.findAllPinned().onSuccessTask(new Continuation, Task>() { + @Override + public Task then(Task> task) throws Exception { + List pins = task.getResult(); + + List> tasks = new ArrayList<>(); + for (EventuallyPin pin : pins) { + tasks.add(pin.unpinInBackground(EventuallyPin.PIN_NAME)); + } + return Task.whenAll(tasks); + } + }); + } + }); + } + }); + + try { + ParseTaskUtils.wait(task); + } catch (ParseException e) { + throw new IllegalStateException(e); + } + + simulateReboot(); + + resume(); + } + + /** + * Creates a Task that is resolved when all the TaskQueues are "complete". + * + * "Complete" is when all the TaskQueues complete the queue of Tasks that were in it before + * whenAll was invoked. This will not keep track of tasks that are added on after whenAll + * was invoked. + */ + private Task whenAll(Collection taskQueues) { + List> tasks = new ArrayList<>(); + + for (TaskQueue taskQueue : taskQueues) { + Task task = taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return toAwait; + } + }); + + tasks.add(task); + } + + return Task.whenAll(tasks); + } + + private static class PauseException extends Exception { + // This class was intentionally left blank. + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePlugins.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePlugins.java new file mode 100644 index 0000000..4413417 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePlugins.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import java.io.File; +import java.io.IOException; + +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +class ParsePlugins { + + private static final String INSTALLATION_ID_LOCATION = "installationId"; + + private static final Object LOCK = new Object(); + private static ParsePlugins instance; + + static void initialize(Context context, Parse.Configuration configuration) { + ParsePlugins.set(new ParsePlugins(context, configuration)); + } + + static void set(ParsePlugins plugins) { + synchronized (LOCK) { + if (instance != null) { + throw new IllegalStateException("ParsePlugins is already initialized"); + } + instance = plugins; + } + } + + static ParsePlugins get() { + synchronized (LOCK) { + return instance; + } + } + + static void reset() { + synchronized (LOCK) { + instance = null; + } + } + + final Object lock = new Object(); + private final Parse.Configuration configuration; + + private Context applicationContext; + + private InstallationId installationId; + + File parseDir; + File cacheDir; + File filesDir; + + ParseHttpClient restClient; + ParseHttpClient fileClient; + + private ParsePlugins(Context context, Parse.Configuration configuration) { + if (context != null) { + applicationContext = context.getApplicationContext(); + } + this.configuration = configuration; + } + + String applicationId() { + return configuration.applicationId; + } + + String clientKey() { + return configuration.clientKey; + } + + Parse.Configuration configuration() { + return configuration; + } + + Context applicationContext() { + return applicationContext; + } + + ParseHttpClient fileClient() { + synchronized (lock) { + if (fileClient == null) { + fileClient = ParseHttpClient.createClient(configuration.clientBuilder); + } + return fileClient; + } + } + + ParseHttpClient restClient() { + synchronized (lock) { + if (restClient == null) { + OkHttpClient.Builder clientBuilder = configuration.clientBuilder; + if (clientBuilder == null) { + clientBuilder = new OkHttpClient.Builder(); + } + //add it as the first interceptor + clientBuilder.interceptors().add(0, new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Headers.Builder headersBuilder = request.headers().newBuilder() + .set(ParseRESTCommand.HEADER_APPLICATION_ID, configuration.applicationId) + .set(ParseRESTCommand.HEADER_CLIENT_VERSION, Parse.externalVersionName()) + .set(ParseRESTCommand.HEADER_APP_BUILD_VERSION, + String.valueOf(ManifestInfo.getVersionCode())) + .set(ParseRESTCommand.HEADER_APP_DISPLAY_VERSION, + ManifestInfo.getVersionName()) + .set(ParseRESTCommand.HEADER_OS_VERSION, Build.VERSION.RELEASE) + .set(ParseRESTCommand.USER_AGENT, userAgent()); + if (request.header(ParseRESTCommand.HEADER_INSTALLATION_ID) == null) { + // We can do this synchronously since the caller is already on a background thread + headersBuilder.set(ParseRESTCommand.HEADER_INSTALLATION_ID, installationId().get()); + } + // client key can be null with self-hosted Parse Server + if (configuration.clientKey != null) { + headersBuilder.set(ParseRESTCommand.HEADER_CLIENT_KEY, configuration.clientKey); + } + request = request.newBuilder() + .headers(headersBuilder.build()) + .build(); + return chain.proceed(request); + } + }); + restClient = ParseHttpClient.createClient(clientBuilder); + } + return restClient; + } + } + + String userAgent() { + String packageVersion = "unknown"; + try { + String packageName = applicationContext.getPackageName(); + int versionCode = applicationContext + .getPackageManager() + .getPackageInfo(packageName, 0) + .versionCode; + packageVersion = packageName + "/" + versionCode; + } catch (PackageManager.NameNotFoundException e) { + // Should never happen. + } + return "Parse Android SDK " + ParseObject.VERSION_NAME + " (" + packageVersion + + ") API Level " + Build.VERSION.SDK_INT; + } + + InstallationId installationId() { + synchronized (lock) { + if (installationId == null) { + //noinspection deprecation + installationId = new InstallationId(new File(getParseDir(), INSTALLATION_ID_LOCATION)); + } + return installationId; + } + } + + @Deprecated + File getParseDir() { + synchronized (lock) { + if (parseDir == null) { + parseDir = applicationContext.getDir("Parse", Context.MODE_PRIVATE); + } + return createFileDir(parseDir); + } + } + + File getCacheDir() { + synchronized (lock) { + if (cacheDir == null) { + cacheDir = new File(applicationContext.getCacheDir(), "com.parse"); + } + return createFileDir(cacheDir); + } + } + + File getFilesDir() { + synchronized (lock) { + if (filesDir == null) { + filesDir = new File(applicationContext.getFilesDir(), "com.parse"); + } + return createFileDir(filesDir); + } + } + + private static File createFileDir(File file) { + if (!file.exists()) { + if (!file.mkdirs()) { + return file; + } + } + return file; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePolygon.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePolygon.java new file mode 100644 index 0000000..0a07f96 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePolygon.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.location.Criteria; +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.Locale; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** + * {@code ParsePolygon} represents a set of coordinates that may be associated with a key + * in a {@link ParseObject} or used as a reference point for geo queries. This allows proximity + * based queries on the key. + * + * Example: + *
+ * List points = new ArrayList();
+ * points.add(new ParseGeoPoint(0,0));
+ * points.add(new ParseGeoPoint(0,1));
+ * points.add(new ParseGeoPoint(1,1));
+ * points.add(new ParseGeoPoint(1,0));
+ * ParsePolygon polygon = new ParsePolygon(points);
+ * ParseObject object = new ParseObject("PlaceObject");
+ * object.put("area", polygon);
+ * object.save();
+ * 
+ */ + +public class ParsePolygon implements Parcelable { + + private List coordinates; + + /** + * Creates a new polygon with the specified {@link ParseGeoPoint}. + * + * @param coords + * The polygon's coordinates. + */ + public ParsePolygon(List coords) { + setCoordinates(coords); + } + + /** + * Creates a copy of {@code polygon}; + * + * @param polygon + * The polygon to copy. + */ + public ParsePolygon(ParsePolygon polygon) { + this(polygon.getCoordinates()); + } + + /** + * Creates a new point instance from a {@link Parcel} source. This is used when unparceling a + * ParsePolygon. Subclasses that need Parcelable behavior should provide their own + * {@link android.os.Parcelable.Creator} and override this constructor. + * + * @param source The recovered parcel. + */ + protected ParsePolygon(Parcel source) { + this(source, ParseParcelDecoder.get()); + } + + /** + * Creates a new point instance from a {@link Parcel} using the given {@link ParseParcelDecoder}. + * The decoder is currently unused, but it might be in the future, plus this is the pattern we + * are using in parcelable classes. + * + * @param source the parcel + * @param decoder the decoder + */ + ParsePolygon(Parcel source, ParseParcelDecoder decoder) { + setCoordinates(source.readArrayList(null)); + } + + + /** + * Set coordinates. Valid are Array of GeoPoint, ParseGeoPoint or Location + * at least 3 points + * + * @param coords + * The polygon's coordinates. + */ + public void setCoordinates(List coords) { + this.coordinates = ParsePolygon.validate(coords); + } + + /** + * Get coordinates. + */ + public List getCoordinates() { + return coordinates; + } + + /** + * Get converts coordinate to JSONArray. + */ + protected JSONArray coordinatesToJSONArray() throws JSONException{ + JSONArray points = new JSONArray(); + for (ParseGeoPoint coordinate : coordinates) { + JSONArray point = new JSONArray(); + point.put(coordinate.getLatitude()); + point.put(coordinate.getLongitude()); + points.put(point); + } + return points; + } + + /** + * Checks if this {@code ParsePolygon}; contains {@link ParseGeoPoint}. + */ + public boolean containsPoint(ParseGeoPoint point) { + double minX = coordinates.get(0).getLatitude(); + double maxX = coordinates.get(0).getLatitude(); + double minY = coordinates.get(0).getLongitude(); + double maxY = coordinates.get(0).getLongitude(); + + for ( int i = 1; i < coordinates.size(); i += 1) { + ParseGeoPoint geoPoint = coordinates.get(i); + minX = Math.min(geoPoint.getLatitude(), minX); + maxX = Math.max(geoPoint.getLatitude(), maxX); + minY = Math.min(geoPoint.getLongitude(), minY); + maxY = Math.max(geoPoint.getLongitude(), maxY); + } + + boolean outside = point.getLatitude() < minX || point.getLatitude() > maxX || point.getLongitude() < minY || point.getLongitude() > maxY; + if (outside) { + return false; + } + + boolean inside = false; + for (int i = 0, j = coordinates.size() - 1 ; i < coordinates.size(); j = i++) { + double startX = coordinates.get(i).getLatitude(); + double startY = coordinates.get(i).getLongitude(); + double endX = coordinates.get(j).getLatitude(); + double endY = coordinates.get(j).getLongitude(); + + boolean intersect = (( startY > point.getLongitude() ) != ( endY > point.getLongitude() ) && + point.getLatitude() < ( endX - startX ) * ( point.getLongitude() - startY ) / ( endY - startY ) + startX); + + if (intersect) { + inside = !inside; + } + } + return inside; + } + + /** + * Throws exception for invalid coordinates. + */ + static List validate(List coords) { + if (coords.size() < 3) { + throw new IllegalArgumentException("Polygon must have at least 3 GeoPoints"); + } + return coords; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ParsePolygon)) { + return false; + } + if (obj == this) { + return true; + } + ParsePolygon other = (ParsePolygon) obj; + + if (coordinates.size() != other.getCoordinates().size()) { + return false; + } + + boolean isEqual = true; + for (int i = 0; i < coordinates.size(); i += 1) { + if (coordinates.get(i).getLatitude() != other.getCoordinates().get(i).getLatitude() || + coordinates.get(i).getLongitude() != other.getCoordinates().get(i).getLongitude()) { + isEqual = false; + break; + } + } + return isEqual; + } + + @Override + public String toString() { + return String.format(Locale.US, "ParsePolygon: %s", coordinates); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, ParseParcelEncoder.get()); + } + + void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + dest.writeList(coordinates); + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParsePolygon createFromParcel(Parcel source) { + return new ParsePolygon(source, ParseParcelDecoder.get()); + } + + @Override + public ParsePolygon[] newArray(int size) { + return new ParsePolygon[size]; + } + }; +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePush.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePush.java new file mode 100644 index 0000000..a305a31 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePush.java @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import bolts.Continuation; +import bolts.Task; + +/** + * The {@code ParsePush} is a local representation of data that can be sent as a push notification. + *

+ * The typical workflow for sending a push notification from the client is to construct a new + * {@code ParsePush}, use the setter functions to fill it with data, and then use + * {@link #sendInBackground()} to send it. + */ +public class ParsePush { + + /* package for test */ static String KEY_DATA_MESSAGE = "alert"; + /* package for test */ static ParsePushController getPushController() { + return ParseCorePlugins.getInstance().getPushController(); + } + + /* package for test */ static ParsePushChannelsController getPushChannelsController() { + return ParseCorePlugins.getInstance().getPushChannelsController(); + } + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + private static void checkArgument(boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + } + + /* package */ static class State { + + /* package */ static class Builder { + + private Set channelSet; + private ParseQuery query; + private Long expirationTime; + private Long expirationTimeInterval; + private Long pushTime; + private Boolean pushToIOS; + private Boolean pushToAndroid; + private JSONObject data; + + public Builder() { + // do nothing + } + + public Builder(State state) { + this.channelSet = state.channelSet() == null + ? null + : Collections.unmodifiableSet(new HashSet<>(state.channelSet())); + this.query = state.queryState() == null + ? null + : new ParseQuery<>(new ParseQuery.State.Builder(state.queryState())); + this.expirationTime = state.expirationTime(); + this.expirationTimeInterval = state.expirationTimeInterval(); + this.pushTime = state.pushTime(); + this.pushToIOS = state.pushToIOS(); + this.pushToAndroid = state.pushToAndroid(); + // Since in state.build() we check data is not null, we do not need to check it again here. + JSONObject copyData = null; + try { + copyData = new JSONObject(state.data().toString()); + } catch (JSONException e) { + // Swallow this silently since it is impossible to happen + } + this.data = copyData; + } + + public Builder expirationTime(Long expirationTime) { + this.expirationTime = expirationTime; + expirationTimeInterval = null; + return this; + } + + public Builder expirationTimeInterval(Long expirationTimeInterval) { + this.expirationTimeInterval = expirationTimeInterval; + expirationTime = null; + return this; + } + + public Builder pushTime(Long pushTime) { + if (pushTime != null) { + long now = System.currentTimeMillis() / 1000; + long twoWeeks = 60*60*24*7*2; + checkArgument(pushTime > now, "Scheduled push time can not be in the past"); + checkArgument(pushTime < now + twoWeeks, "Scheduled push time can not be more than " + + "two weeks in the future"); + } + this.pushTime = pushTime; + return this; + } + + public Builder pushToIOS(Boolean pushToIOS) { + checkArgument(query == null, "Cannot set push targets (i.e. setPushToAndroid or " + + "setPushToIOS) when pushing to a query"); + this.pushToIOS = pushToIOS; + return this; + } + + public Builder pushToAndroid(Boolean pushToAndroid) { + checkArgument(query == null, "Cannot set push targets (i.e. setPushToAndroid or " + + "setPushToIOS) when pushing to a query"); + this.pushToAndroid = pushToAndroid; + return this; + } + + public Builder data(JSONObject data) { + this.data = data; + return this; + } + + public Builder channelSet(Collection channelSet) { + checkArgument(channelSet != null, "channels collection cannot be null"); + for (String channel : channelSet) { + checkArgument(channel != null, "channel cannot be null"); + } + this.channelSet = new HashSet<>(channelSet); + query = null; + return this; + } + + public Builder query(ParseQuery query) { + checkArgument(query != null, "Cannot target a null query"); + checkArgument(pushToIOS == null && pushToAndroid == null, "Cannot set push targets " + + "(i.e. setPushToAndroid or setPushToIOS) when pushing to a query"); + checkArgument( + query.getClassName().equals( + getSubclassingController().getClassName(ParseInstallation.class)), + "Can only push to a query for Installations"); + channelSet = null; + this.query = query; + return this; + } + + public State build() { + if (data == null) { + throw new IllegalArgumentException( + "Cannot send a push without calling either setMessage or setData"); + } + return new State(this); + } + } + + private final Set channelSet; + private final ParseQuery.State queryState; + private final Long expirationTime; + private final Long expirationTimeInterval; + private final Long pushTime; + private final Boolean pushToIOS; + private final Boolean pushToAndroid; + private final JSONObject data; + + private State(Builder builder) { + this.channelSet = builder.channelSet == null ? + null : Collections.unmodifiableSet(new HashSet<>(builder.channelSet)); + this.queryState = builder.query == null ? null : builder.query.getBuilder().build(); + this.expirationTime = builder.expirationTime; + this.expirationTimeInterval = builder.expirationTimeInterval; + this.pushTime = builder.pushTime; + this.pushToIOS = builder.pushToIOS; + this.pushToAndroid = builder.pushToAndroid; + // Since in builder.build() we check data is not null, we do not need to check it again here. + JSONObject copyData = null; + try { + copyData = new JSONObject(builder.data.toString()); + } catch (JSONException e) { + // Swallow this silently since it is impossible to happen + } + this.data = copyData; + } + + public Set channelSet() { + return channelSet; + } + + public ParseQuery.State queryState() { + return queryState; + } + + public Long expirationTime() { + return expirationTime; + } + + public Long expirationTimeInterval() { + return expirationTimeInterval; + } + + public Long pushTime() { + return pushTime; + } + + public Boolean pushToIOS() { + return pushToIOS; + } + + public Boolean pushToAndroid() { + return pushToAndroid; + } + + public JSONObject data() { + // Since in builder.build() we check data is not null, we do not need to check it again here. + JSONObject copyData = null; + try { + copyData = new JSONObject(data.toString()); + } catch (JSONException e) { + // Swallow this exception silently since it is impossible to happen + } + return copyData; + } + } + + private static final String TAG = "com.parse.ParsePush"; + + /* package for test */ final State.Builder builder; + + /** + * Creates a new push notification. + * + * The default channel is the empty string, also known as the global broadcast channel, but this + * value can be overridden using {@link #setChannel(String)}, {@link #setChannels(Collection)} or + * {@link #setQuery(ParseQuery)}. Before sending the push notification you must call either + * {@link #setMessage(String)} or {@link #setData(JSONObject)}. + */ + public ParsePush() { + this(new State.Builder()); + } + + /** + * Creates a copy of {@code push}. + * + * @param push + * The push to copy. + */ + public ParsePush(ParsePush push) { + this(new State.Builder(push.builder.build())); + } + + private ParsePush(State.Builder builder) { + this.builder = builder; + } + + /** + * Adds 'channel' to the 'channels' list in the current {@link ParseInstallation} and saves it in + * a background thread. + * + * @param channel + * The channel to subscribe to. + * @return A Task that is resolved when the the subscription is complete. + */ + public static Task subscribeInBackground(String channel) { + return getPushChannelsController().subscribeInBackground(channel); + } + + /** + * Adds 'channel' to the 'channels' list in the current {@link ParseInstallation} and saves it in + * a background thread. + * + * @param channel + * The channel to subscribe to. + * @param callback + * The SaveCallback that is called after the Installation is saved. + */ + public static void subscribeInBackground(String channel, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(subscribeInBackground(channel), callback); + } + + /** + * Removes 'channel' from the 'channels' list in the current {@link ParseInstallation} and saves + * it in a background thread. + * + * @param channel + * The channel to unsubscribe from. + * @return A Task that is resolved when the the unsubscription is complete. + */ + public static Task unsubscribeInBackground(String channel) { + return getPushChannelsController().unsubscribeInBackground(channel); + } + /** + * Removes 'channel' from the 'channels' list in the current {@link ParseInstallation} and saves + * it in a background thread. + * + * @param channel + * The channel to unsubscribe from. + * @param callback + * The SaveCallback that is called after the Installation is saved. + */ + public static void unsubscribeInBackground(String channel, SaveCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(unsubscribeInBackground(channel), callback); + } + + /** + * A helper method to concisely send a push message to a query. This method is equivalent to + * ParsePush push = new ParsePush(); push.setMessage(message); push.setQuery(query); + * push.sendInBackground(); + * + * @param message + * The message that will be shown in the notification. + * @param query + * A ParseInstallation query which specifies the recipients of a push. + * @return A task that is resolved when the message is sent. + */ + public static Task sendMessageInBackground(String message, + ParseQuery query) { + ParsePush push = new ParsePush(); + push.setQuery(query); + push.setMessage(message); + return push.sendInBackground(); + } + + /** + * A helper method to concisely send a push message to a query. This method is equivalent to + * ParsePush push = new ParsePush(); push.setMessage(message); push.setQuery(query); + * push.sendInBackground(callback); + * + * @param message + * The message that will be shown in the notification. + * @param query + * A ParseInstallation query which specifies the recipients of a push. + * @param callback + * callback.done(e) is called when the send completes. + */ + public static void sendMessageInBackground(String message, ParseQuery query, + SendCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(sendMessageInBackground(message, query), callback); + } + + /** + * A helper method to concisely send a push to a query. This method is equivalent to ParsePush + * push = new ParsePush(); push.setData(data); push.setQuery(query); push.sendInBackground(); + * + * @param data + * The entire data of the push message. See the push guide for more details on the data + * format. + * @param query + * A ParseInstallation query which specifies the recipients of a push. + * @return A task that is resolved when the data is sent. + */ + public static Task sendDataInBackground(JSONObject data, + ParseQuery query) { + ParsePush push = new ParsePush(); + push.setQuery(query); + push.setData(data); + return push.sendInBackground(); + } + + /** + * A helper method to concisely send a push to a query. This method is equivalent to ParsePush + * push = new ParsePush(); push.setData(data); push.setQuery(query); + * push.sendInBackground(callback); + * + * @param data + * The entire data of the push message. See the push guide for more details on the data + * format. + * @param query + * A ParseInstallation query which specifies the recipients of a push. + * @param callback + * callback.done(e) is called when the send completes. + */ + public static void sendDataInBackground(JSONObject data, ParseQuery query, + SendCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(sendDataInBackground(data, query), callback); + } + + /** + * Sets the channel on which this push notification will be sent. The channel name must start with + * a letter and contain only letters, numbers, dashes, and underscores. A push can either have + * channels or a query. Setting this will unset the query. + */ + public void setChannel(String channel) { + builder.channelSet(Collections.singletonList(channel)); + } + + /** + * Sets the collection of channels on which this push notification will be sent. Each channel name + * must start with a letter and contain only letters, numbers, dashes, and underscores. A push can + * either have channels or a query. Setting this will unset the query. + */ + public void setChannels(Collection channels) { + builder.channelSet(channels); + } + + /** + * Sets the query for this push for which this push notification will be sent. This query will be + * executed in the Parse cloud; this push notification will be sent to Installations which this + * query yields. A push can either have channels or a query. Setting this will unset the channels. + * + * @param query + * A query to which this push should target. This must be a ParseInstallation query. + */ + public void setQuery(ParseQuery query) { + builder.query(query); + } + + /** + * Sets a UNIX epoch timestamp at which this notification should expire, in seconds (UTC). This + * notification will be sent to devices which are either online at the time the notification is + * sent, or which come online before the expiration time is reached. Because device clocks are not + * guaranteed to be accurate, most applications should instead use + * {@link #setExpirationTimeInterval(long)}. + */ + public void setExpirationTime(long time) { + builder.expirationTime(time); + } + + /** + * Sets the time interval after which this notification should expire, in seconds. This + * notification will be sent to devices which are either online at the time the notification is + * sent, or which come online within the given number of seconds of the notification being + * received by Parse's server. An interval which is less than or equal to zero indicates that the + * message should only be sent to devices which are currently online. + */ + public void setExpirationTimeInterval(long timeInterval) { + builder.expirationTimeInterval(timeInterval); + } + + /** + * Clears both expiration values, indicating that the notification should never expire. + */ + public void clearExpiration() { + builder.expirationTime(null); + builder.expirationTimeInterval(null); + } + + /** + * Sets a UNIX epoch timestamp at which this notification should be delivered, in seconds (UTC). + * Scheduled time can not be in the past and must be at most two weeks in the future. + */ + public void setPushTime(long time) { + builder.pushTime(time); + } + + /** + * Set whether this push notification will go to iOS devices. + *

+ * Setting this to {@code true} will set {@link #setPushToAndroid(boolean)} to {@code false}. + *

+ * Note: You must set up iOS push certificates before sending pushes to iOS. + * + * @deprecated Please use {@link #setQuery(ParseQuery)} with a {@link ParseQuery} targeting + * {@link ParseInstallation}s with a constraint on the {@code deviceType} field. If you use + * {@code #setPushToIOS(boolean)} or {@link #setPushToAndroid(boolean)}, then you will only be + * able to send to one of these two device types (e.g. and not Windows). + */ + @Deprecated + public void setPushToIOS(boolean pushToIOS) { + builder.pushToIOS(pushToIOS); + } + + /** + * Set whether this push notification will go to Android devices. + *

+ * Setting this to {@code true} will set {@link #setPushToIOS(boolean)} to {@code false}. + * + * @deprecated Please use {@link #setQuery(ParseQuery)} with a {@link ParseQuery} targeting + * {@link ParseInstallation}s with a constraint on the {@code deviceType} field. If you use + * {@code #setPushToAndroid(boolean)} or {@link #setPushToIOS(boolean)}, then you will only be + * able to send to one of these two device types (e.g. and not Windows). + */ + @Deprecated + public void setPushToAndroid(boolean pushToAndroid) { + builder.pushToAndroid(pushToAndroid); + } + + /** + * Sets the entire data of the push message. See the push guide for more details on the data + * format. This will overwrite any data specified in {@link #setMessage(String)}. + */ + public void setData(JSONObject data) { + builder.data(data); + } + + /** + * Sets the message that will be shown in the notification. This will overwrite any data specified + * in {@link #setData(JSONObject)}. + */ + public void setMessage(String message) { + JSONObject data = new JSONObject(); + try { + data.put(KEY_DATA_MESSAGE, message); + } catch (JSONException e) { + PLog.e(TAG, "JSONException in setMessage", e); + } + setData(data); + } + + /** + * Sends this push notification in a background thread. Use this when you do not have code to run + * on completion of the push. + * + * @return A Task is resolved when the push has been sent. + */ + public Task sendInBackground() { + // Since getCurrentSessionTokenAsync takes time, we build the state before it. + final State state = builder.build(); + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + return getPushController().sendInBackground(state, sessionToken); + } + }); + } + + /** + * Sends this push notification while blocking this thread until the push notification has + * successfully reached the Parse servers. Typically, you should use {@link #sendInBackground()} + * instead of this, unless you are managing your own threading. + * + * @throws ParseException + * Throws an exception if the server is inaccessible. + */ + public void send() throws ParseException { + ParseTaskUtils.wait(sendInBackground()); + } + + /** + * Sends this push notification in a background thread. This is preferable to using + * send(), unless your code is already running from a background thread. + * + * @param callback + * callback.done(e) is called when the send completes. + */ + public void sendInBackground(SendCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(sendInBackground(), callback); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java new file mode 100644 index 0000000..8aa0696 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * A {@link BroadcastReceiver} for rendering and reacting to to Notifications. + *

+ * This {@link BroadcastReceiver} must be registered in order to use the {@link ParsePush} + * subscription methods. As a security precaution, the intent filters for this + * {@link BroadcastReceiver} must not be exported. Add the following lines to your + * {@code AndroidManifest.xml} file, inside the <application> element to properly register the + * {@code ParsePushBroadcastReceiver}: + *

+ *

+ * <receiver android:name="com.parse.ParsePushBroadcastReceiver" android:exported=false>
+ *  <intent-filter>
+ *     <action android:name="com.parse.push.intent.RECEIVE" />
+ *     <action android:name="com.parse.push.intent.OPEN" />
+ *     <action android:name="com.parse.push.intent.DELETE" />
+ *   </intent-filter>
+ * </receiver>
+ * 
+ *

+ * The {@code ParsePushBroadcastReceiver} is designed to provide maximal configurability with + * minimal effort. To customize the push icon, add the following line as a child of your + * <application> element: + *

+ *

+ *   <meta-data android:name="com.parse.push.notification_icon"
+ *              android:resource="@drawable/icon"/>
+ * 
+ *

+ * where {@code drawable/icon} may be the path to any drawable resource. The + * Android style + * guide for Notifications suggests that push icons should be flat monochromatic images. + *

+ * To achieve further customization, {@code ParsePushBroadcastReceiver} can be subclassed. When + * providing your own implementation of {@code ParsePushBroadcastReceiver}, be sure to change + * {@code com.parse.PushBroadcastReceiver} to the name of your custom subclass in your + * AndroidManifest.xml. You can intercept and override the behavior of entire portions of the + * push lifecycle by overriding {@link #onPushReceive(Context, Intent)}, + * {@link #onPushOpen(Context, Intent)}, or {@link #onPushDismiss(Context, Intent)}. + * To make minor changes to the appearance of a notification, override + * {@link #getSmallIconId(Context, Intent)} or {@link #getLargeIcon(Context, Intent)}. To completely + * change the Notification generated, override {@link #getNotification(Context, Intent)}. To + * change the NotificationChannel generated, override {@link #getNotificationChannel(Context, Intent)}. To + * change how the NotificationChannel is created, override {@link #createNotificationChannel(Context, NotificationChannel)}. + * To change the Activity launched when a user opens a Notification, override + * {@link #getActivity(Context, Intent)}. + */ +// Hack note: Javadoc smashes the last two paragraphs together without the

tags. +public class ParsePushBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "com.parse.ParsePushReceiver"; + + /** + * The name of the Intent extra which contains a channel used to route this notification. + * May be {@code null}. + * */ + public static final String KEY_PUSH_CHANNEL = "com.parse.Channel"; + + /** The name of the Intent extra which contains the JSON payload of the Notification. */ + public static final String KEY_PUSH_DATA = "com.parse.Data"; + + /** The name of the Intent fired when a push has been received. */ + public static final String ACTION_PUSH_RECEIVE = "com.parse.push.intent.RECEIVE"; + + /** The name of the Intent fired when a notification has been opened. */ + public static final String ACTION_PUSH_OPEN = "com.parse.push.intent.OPEN"; + + /** The name of the Intent fired when a notification has been dismissed. */ + public static final String ACTION_PUSH_DELETE = "com.parse.push.intent.DELETE"; + + /** The name of the meta-data field used to override the icon used in Notifications. */ + public static final String PROPERTY_PUSH_ICON = "com.parse.push.notification_icon"; + + protected static final int SMALL_NOTIFICATION_MAX_CHARACTER_LIMIT = 38; + + private static final List REQUIRED_ACTIONS = Arrays.asList( + ACTION_PUSH_RECEIVE, ACTION_PUSH_OPEN, ACTION_PUSH_DELETE); + + /** + * Called at startup at the moment of parsing the manifest, to see + * if it was correctly set-up. + */ + static boolean isSupported() { + int actions = 0; + for (String action : REQUIRED_ACTIONS) { + if (ManifestInfo.hasIntentReceiver(action)) actions++; + } + + if (actions < REQUIRED_ACTIONS.size()) { + if (actions > 0) { + throw new IllegalStateException( + "The Parse Push BroadcastReceiver must implement a filter for all of " + + ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE + ", " + + ParsePushBroadcastReceiver.ACTION_PUSH_OPEN + ", and " + + ParsePushBroadcastReceiver.ACTION_PUSH_DELETE); + } else { + PLog.e(TAG, "Push is currently disabled. Parse SDK requires your app to " + + "have a BroadcastReceiver that handles " + + ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE + ", " + + ParsePushBroadcastReceiver.ACTION_PUSH_OPEN + ", and " + + ParsePushBroadcastReceiver.ACTION_PUSH_DELETE + ". You can do this by adding " + + "these lines to your AndroidManifest.xml:\n\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "); + } + return false; + } + return true; + } + + /** + * Delegates the generic {@code onReceive} event to a notification lifecycle event. + * Subclasses are advised to override the lifecycle events and not this method. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * + * @see ParsePushBroadcastReceiver#onPushReceive(Context, Intent) + * @see ParsePushBroadcastReceiver#onPushOpen(Context, Intent) + * @see ParsePushBroadcastReceiver#onPushDismiss(Context, Intent) + */ + @Override + public void onReceive(Context context, Intent intent) { + String intentAction = intent.getAction(); + switch (intentAction) { + case ACTION_PUSH_RECEIVE: + onPushReceive(context, intent); + break; + case ACTION_PUSH_DELETE: + onPushDismiss(context, intent); + break; + case ACTION_PUSH_OPEN: + onPushOpen(context, intent); + break; + } + } + + /** + * Called when the push notification is received. By default, a broadcast intent will be sent if + * an "action" is present in the data and a notification will be show if "alert" and "title" are + * present in the data. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + */ + protected void onPushReceive(Context context, Intent intent) { + String pushDataStr = intent.getStringExtra(KEY_PUSH_DATA); + if (pushDataStr == null) { + PLog.e(TAG, "Can not get push data from intent."); + return; + } + PLog.v(TAG, "Received push data: " + pushDataStr); + + JSONObject pushData = null; + try { + pushData = new JSONObject(pushDataStr); + } catch (JSONException e) { + PLog.e(TAG, "Unexpected JSONException when receiving push data: ", e); + } + + // If the push data includes an action string, that broadcast intent is fired. + String action = null; + if (pushData != null) { + action = pushData.optString("action", null); + } + if (action != null) { + Bundle extras = intent.getExtras(); + Intent broadcastIntent = new Intent(); + broadcastIntent.putExtras(extras); + broadcastIntent.setAction(action); + broadcastIntent.setPackage(context.getPackageName()); + context.sendBroadcast(broadcastIntent); + } + + Notification notification = getNotification(context, intent); + + if (notification != null) { + ParseNotificationManager.getInstance().showNotification(context, notification); + } + } + + /** + * Called when the push notification is dismissed. By default, nothing is performed + * on notification dismissal. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + */ + protected void onPushDismiss(Context context, Intent intent) { + // do nothing + } + + /** + * Called when the push notification is opened by the user. Sends analytics info back to Parse + * that the application was opened from this push notification. By default, this will navigate + * to the {@link Activity} returned by {@link #getActivity(Context, Intent)}. If the push contains + * a 'uri' parameter, an Intent is fired to view that URI with the Activity returned by + * {@link #getActivity} in the back stack. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + */ + protected void onPushOpen(Context context, Intent intent) { + // Send a Parse Analytics "push opened" event + ParseAnalytics.trackAppOpenedInBackground(intent); + + String uriString = null; + try { + JSONObject pushData = new JSONObject(intent.getStringExtra(KEY_PUSH_DATA)); + uriString = pushData.optString("uri", null); + } catch (JSONException e) { + PLog.e(TAG, "Unexpected JSONException when receiving push data: ", e); + } + + Class cls = getActivity(context, intent); + Intent activityIntent; + if (uriString != null) { + activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uriString)); + } else { + activityIntent = new Intent(context, cls); + } + + activityIntent.putExtras(intent.getExtras()); + /* + In order to remove dependency on android-support-library-v4 + The reason why we differentiate between versions instead of just using context.startActivity + for all devices is because in API 11 the recommended conventions for app navigation using + the back key changed. + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + TaskStackBuilderHelper.startActivities(context, cls, activityIntent); + } else { + activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(activityIntent); + } + } + + /** + * Used by {@link #onPushOpen} to determine which activity to launch or insert into the back + * stack. The default implementation retrieves the launch activity class for the package. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * @return + * The default {@code Activity} class of the package or {@code null} if no launch intent is + * defined in {@code AndroidManifest.xml}. + */ + protected Class getActivity(Context context, Intent intent) { + String packageName = context.getPackageName(); + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + if (launchIntent == null) { + return null; + } + String className = launchIntent.getComponent().getClassName(); + Class cls = null; + try { + cls = (Class )Class.forName(className); + } catch (ClassNotFoundException e) { + // do nothing + } + return cls; + } + + /** + * Retrieves the channel to be used in a {@link Notification} if API >= 26, if not null. The default returns a new channel + * with id "parse_push", name "Push notifications" and default importance. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * @return + * The notification channel + */ + @TargetApi(Build.VERSION_CODES.O) + protected NotificationChannel getNotificationChannel(Context context, Intent intent) { + return new NotificationChannel("parse_push", "Push notifications", NotificationManager.IMPORTANCE_DEFAULT); + } + + /** + * Creates the notification channel with the NotificationManager. Channel is not recreated + * if the channel properties are unchanged. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param notificationChannel + * The {@code NotificationChannel} to be created. + */ + @TargetApi(Build.VERSION_CODES.O) + protected void createNotificationChannel(Context context, NotificationChannel notificationChannel) { + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.createNotificationChannel(notificationChannel); + } + + /** + * Retrieves the small icon to be used in a {@link Notification}. The default implementation uses + * the icon specified by {@code com.parse.push.notification_icon} {@code meta-data} in your + * {@code AndroidManifest.xml} with a fallback to the launcher icon for this package. To conform + * to Android style guides, it is highly recommended that developers specify an explicit push + * icon. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * @return + * The resource id of the default small icon for the package + * + * @see Android Notification Style Guide + */ + protected int getSmallIconId(Context context, Intent intent) { + Bundle metaData = ManifestInfo.getApplicationMetadata(context); + int explicitId = 0; + if (metaData != null) { + explicitId = metaData.getInt(PROPERTY_PUSH_ICON); + } + return explicitId != 0 ? explicitId : ManifestInfo.getIconId(); + } + + /** + * Retrieves the large icon to be used in a {@link Notification}. This {@code Bitmap} should be + * used to provide special context for a particular {@link Notification}, such as the avatar of + * user who generated the {@link Notification}. The default implementation returns {@code null}, + * causing the {@link Notification} to display only the small icon. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * @return + * Bitmap of the default large icon for the package + * + * @see Android Notification UI Overview + */ + protected Bitmap getLargeIcon(Context context, Intent intent) { + return null; + } + + private JSONObject getPushData(Intent intent) { + try { + return new JSONObject(intent.getStringExtra(KEY_PUSH_DATA)); + } catch (JSONException e) { + PLog.e(TAG, "Unexpected JSONException when receiving push data: ", e); + return null; + } + } + /** + * Creates a {@link Notification} with reasonable defaults. If "alert" and "title" are + * both missing from data, then returns {@code null}. If the text in the notification is longer + * than 38 characters long, the style of the notification will be set to + * {@link android.app.Notification.BigTextStyle}. + *

+ * As a security precaution, developers overriding this method should be sure to set the package + * on notification {@code Intent}s to avoid leaking information to other apps. + * + * @param context + * The {@code Context} in which the receiver is running. + * @param intent + * An {@code Intent} containing the channel and data of the current push notification. + * @return + * The notification to be displayed. + * + * @see ParsePushBroadcastReceiver#onPushReceive(Context, Intent) + */ + protected Notification getNotification(Context context, Intent intent) { + JSONObject pushData = getPushData(intent); + if (pushData == null || (!pushData.has("alert") && !pushData.has("title"))) { + return null; + } + + String title = pushData.optString("title", ManifestInfo.getDisplayName(context)); + String alert = pushData.optString("alert", "Notification received."); + String tickerText = String.format(Locale.getDefault(), "%s: %s", title, alert); + + Bundle extras = intent.getExtras(); + + Random random = new Random(); + int contentIntentRequestCode = random.nextInt(); + int deleteIntentRequestCode = random.nextInt(); + + // Security consideration: To protect the app from tampering, we require that intent filters + // not be exported. To protect the app from information leaks, we restrict the packages which + // may intercept the push intents. + String packageName = context.getPackageName(); + + Intent contentIntent = new Intent(ParsePushBroadcastReceiver.ACTION_PUSH_OPEN); + contentIntent.putExtras(extras); + contentIntent.setPackage(packageName); + + Intent deleteIntent = new Intent(ParsePushBroadcastReceiver.ACTION_PUSH_DELETE); + deleteIntent.putExtras(extras); + deleteIntent.setPackage(packageName); + + PendingIntent pContentIntent = PendingIntent.getBroadcast(context, contentIntentRequestCode, + contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pDeleteIntent = PendingIntent.getBroadcast(context, deleteIntentRequestCode, + deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + + + // The purpose of setDefaults(Notification.DEFAULT_ALL) is to inherit notification properties + // from system defaults + NotificationCompat.Builder parseBuilder = new NotificationCompat.Builder(context); + parseBuilder.setContentTitle(title) + .setContentText(alert) + .setTicker(tickerText) + .setSmallIcon(this.getSmallIconId(context, intent)) + .setLargeIcon(this.getLargeIcon(context, intent)) + .setContentIntent(pContentIntent) + .setDeleteIntent(pDeleteIntent) + .setAutoCancel(true) + .setDefaults(Notification.DEFAULT_ALL); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = getNotificationChannel(context, intent); + createNotificationChannel(context, notificationChannel); + parseBuilder.setNotificationChannel(notificationChannel.getId()); + } + + if (alert != null + && alert.length() > ParsePushBroadcastReceiver.SMALL_NOTIFICATION_MAX_CHARACTER_LIMIT) { + parseBuilder.setStyle(new NotificationCompat.Builder.BigTextStyle().bigText(alert)); + } + return parseBuilder.build(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushChannelsController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushChannelsController.java new file mode 100644 index 0000000..5698a48 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushChannelsController.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Collections; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** package */ class ParsePushChannelsController { + private static final String TAG = "com.parse.ParsePushChannelsController"; + + private static ParseCurrentInstallationController getCurrentInstallationController() { + return ParseCorePlugins.getInstance().getCurrentInstallationController(); + } + + public Task subscribeInBackground(final String channel) { + checkManifestAndLogErrorIfNecessary(); + if (channel == null) { + throw new IllegalArgumentException("Can't subscribe to null channel."); + } + return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseInstallation installation = task.getResult(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + if (channels == null + || installation.isDirty(ParseInstallation.KEY_CHANNELS) + || !channels.contains(channel)) { + installation.addUnique(ParseInstallation.KEY_CHANNELS, channel); + return installation.saveInBackground(); + } else { + return Task.forResult(null); + } + } + }); + } + + public Task unsubscribeInBackground(final String channel) { + checkManifestAndLogErrorIfNecessary(); + if (channel == null) { + throw new IllegalArgumentException("Can't unsubscribe from null channel."); + } + return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseInstallation installation = task.getResult(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + if (channels != null && channels.contains(channel)) { + installation.removeAll( + ParseInstallation.KEY_CHANNELS, Collections.singletonList(channel)); + return installation.saveInBackground(); + } else { + return Task.forResult(null); + } + } + }); + } + + private static boolean loggedManifestError = false; + private static void checkManifestAndLogErrorIfNecessary() { + if (!loggedManifestError && ManifestInfo.getPushType() == PushType.NONE) { + loggedManifestError = true; + PLog.e(TAG, "Tried to subscribe or unsubscribe from a channel, but push is not enabled " + + "correctly. " + ManifestInfo.getPushDisabledMessage()); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushController.java new file mode 100644 index 0000000..8ba83e9 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParsePushController.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import bolts.Task; + +/** package */ class ParsePushController { + + /* package for test */ final static String DEVICE_TYPE_IOS = "ios"; + /* package for test */ final static String DEVICE_TYPE_ANDROID = "android"; + private final ParseHttpClient restClient; + + public ParsePushController(ParseHttpClient restClient) { + this.restClient = restClient; + } + + public Task sendInBackground(ParsePush.State state, String sessionToken) { + return buildRESTSendPushCommand(state, sessionToken).executeAsync(restClient).makeVoid(); + } + + /* package for test */ ParseRESTCommand buildRESTSendPushCommand(ParsePush.State state, + String sessionToken) { + // pushToAndroid & pushToIOS are deprecated. It's OK to err on the side of omitting + // a type constraint because the pusher will do a query rewrite on the backend to + // constraint the query to supported device types only. + String deviceType = null; + if (state.queryState() == null) { + // android is on by default, ios is off by default + boolean willPushToAndroid = state.pushToAndroid() != null && state.pushToAndroid(); + boolean willPushToIOS = state.pushToIOS() != null && state.pushToIOS(); + if (willPushToIOS && willPushToAndroid) { + // Push to both by not including a 'type' parameter + } else if (willPushToIOS) { + deviceType = DEVICE_TYPE_IOS; + } else if (willPushToAndroid) { + deviceType = DEVICE_TYPE_ANDROID; + } + } + return ParseRESTPushCommand.sendPushCommand(state.queryState(), state.channelSet(), deviceType, + state.expirationTime(), state.expirationTimeInterval(), state.pushTime(), state.data(), + sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQuery.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQuery.java new file mode 100644 index 0000000..78f7978 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQuery.java @@ -0,0 +1,2195 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.regex.Pattern; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * The {@code ParseQuery} class defines a query that is used to fetch {@link ParseObject}s. The most + * common use case is finding all objects that match a query through the {@link #findInBackground()} + * method, using a {@link FindCallback}. For example, this sample code fetches all objects of class + * {@code "MyClass"}. It calls a different function depending on whether the fetch succeeded or not. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.findInBackground(new FindCallback<ParseObject>() {
+ *     public void done(List<ParseObject> objects, ParseException e) {
+ *         if (e == null) {
+ *             objectsWereRetrievedSuccessfully(objects);
+ *         } else {
+ *             objectRetrievalFailed();
+ *         }
+ *     }
+ * }
+ * 
+ *

+ * A {@code ParseQuery} can also be used to retrieve a single object whose id is known, through the + * {@link #getInBackground(String)} method, using a {@link GetCallback}. For example, this + * sample code fetches an object of class {@code "MyClass"} and id {@code myId}. It calls + * a different function depending on whether the fetch succeeded or not. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.getInBackground(myId, new GetCallback<ParseObject>() {
+ *     public void done(ParseObject object, ParseException e) {
+ *         if (e == null) {
+ *             objectWasRetrievedSuccessfully(object);
+ *         } else {
+ *             objectRetrievalFailed();
+ *         }
+ *     }
+ * }
+ * 
+ *

+ * A {@code ParseQuery} can also be used to count the number of objects that match the query without + * retrieving all of those objects. For example, this sample code counts the number of objects of + * the class {@code "MyClass"}. + *

+ *

+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.countInBackground(new CountCallback() {
+ *     public void done(int count, ParseException e) {
+ *         if (e == null) {
+ *             objectsWereCounted(count);
+ *         } else {
+ *             objectCountFailed();
+ *         }
+ *     }
+ * }
+ * 
+ *

+ * Using the callback methods is usually preferred because the network operation will not block the + * calling thread. However, in some cases it may be easier to use the {@link #find()}, + * {@link #get(String)} or {@link #count()} calls, which do block the calling thread. For example, + * if your application has already spawned a background task to perform work, that background task + * could use the blocking calls and avoid the code complexity of callbacks. + */ +public class ParseQuery { + + private static ParseQueryController getQueryController() { + return ParseCorePlugins.getInstance().getQueryController(); + } + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + /** + * Constraints for a {@code ParseQuery}'s where clause. A map of field names to constraints. The + * values can either be actual values to compare with for equality, or instances of + * {@link KeyConstraints}. + */ + @SuppressWarnings("serial") + /* package */ static class QueryConstraints extends HashMap { + + public QueryConstraints() { + super(); + } + + public QueryConstraints(Map map) { + super(map); + } + } + + /** + * Constraints for a particular field in a query. If this is used, it's a may where the keys are + * special operators, such as $greaterThan or $nin. The values are the actual values to compare + * against. + */ + @SuppressWarnings("serial") + /* package */ static class KeyConstraints extends HashMap { + } + + /** + * Constraint for a $relatedTo query. + */ + /* package */ static class RelationConstraint { + private String key; + private ParseObject object; + + public RelationConstraint(String key, ParseObject object) { + if (key == null || object == null) { + throw new IllegalArgumentException("Arguments must not be null."); + } + this.key = key; + this.object = object; + } + + public String getKey() { + return key; + } + + public ParseObject getObject() { + return object; + } + + public ParseRelation getRelation() { + return object.getRelation(key); + } + + /** + * Encodes the constraint in a format appropriate for including in the query. + */ + public JSONObject encode(ParseEncoder objectEncoder) { + JSONObject json = new JSONObject(); + try { + json.put("key", key); + json.put("object", objectEncoder.encodeRelatedObject(object)); + } catch (JSONException e) { + // This can never happen. + throw new RuntimeException(e); + } + return json; + } + } + + /** + * Constructs a query that is the {@code or} of the given queries. + * + * @param queries + * The list of {@code ParseQuery}s to 'or' together + * @return A {@code ParseQuery} that is the 'or' of the passed in queries + */ + public static ParseQuery or(List> queries) { + if (queries.isEmpty()) { + throw new IllegalArgumentException("Can't take an or of an empty list of queries"); + } + + List> builders = new ArrayList<>(); + for (ParseQuery query : queries) { + builders.add(query.getBuilder()); + } + return new ParseQuery<>(State.Builder.or(builders)); + } + + /** + * Creates a new query for the given {@link ParseObject} subclass type. A default query with no + * further parameters will retrieve all {@link ParseObject}s of the provided class. + * + * @param subclass + * The {@link ParseObject} subclass type to retrieve. + * @return A new {@code ParseQuery}. + */ + public static ParseQuery getQuery(Class subclass) { + return new ParseQuery<>(subclass); + } + + /** + * Creates a new query for the given class name. A default query with no further parameters will + * retrieve all {@link ParseObject}s of the provided class name. + * + * @param className + * The name of the class to retrieve {@link ParseObject}s for. + * @return A new {@code ParseQuery}. + */ + public static ParseQuery getQuery(String className) { + return new ParseQuery<>(className); + } + + /** + * Constructs a query for {@link ParseUser}s. + * + * @deprecated Please use {@link ParseUser#getQuery()} instead. + */ + @Deprecated + public static ParseQuery getUserQuery() { + return ParseUser.getQuery(); + } + + /** + * {@code CachePolicy} specifies different caching policies that could be used with + * {@link ParseQuery}. + *

+ * This lets you show data when the user's device is offline, or when the app has just started and + * network requests have not yet had time to complete. Parse takes care of automatically flushing + * the cache when it takes up too much space. + *

+ * Note: Cache policy can only be set when Local Datastore is not enabled. + * + * @see com.parse.ParseQuery + */ + public enum CachePolicy { + /** + * The query does not load from the cache or save results to the cache. + *

+ * This is the default cache policy. + */ + IGNORE_CACHE, + + /** + * The query only loads from the cache, ignoring the network. + *

+ * If there are no cached results, this causes a {@link ParseException#CACHE_MISS}. + */ + CACHE_ONLY, + + /** + * The query does not load from the cache, but it will save results to the cache. + */ + NETWORK_ONLY, + + /** + * The query first tries to load from the cache, but if that fails, it loads results from the + * network. + *

+ * If there are no cached results, this causes a {@link ParseException#CACHE_MISS}. + */ + CACHE_ELSE_NETWORK, + + /** + * The query first tries to load from the network, but if that fails, it loads results from the + * cache. + *

+ * If there are no cached results, this causes a {@link ParseException#CACHE_MISS}. + */ + NETWORK_ELSE_CACHE, + + /** + * The query first loads from the cache, then loads from the network. + * The callback will be called twice - first with the cached results, then with the network + * results. Since it returns two results at different times, this cache policy cannot be used + * with synchronous or task methods. + */ + // TODO(grantland): Remove this and come up with a different solution, since it breaks our + // "callbacks get called at most once" paradigm. (v2) + CACHE_THEN_NETWORK + } + + private static void throwIfLDSEnabled() { + throwIfLDSEnabled(false); + } + + private static void throwIfLDSDisabled() { + throwIfLDSEnabled(true); + } + + private static void throwIfLDSEnabled(boolean enabled) { + boolean ldsEnabled = Parse.isLocalDatastoreEnabled(); + if (enabled && !ldsEnabled) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + if (!enabled && ldsEnabled) { + throw new IllegalStateException("Unsupported method when Local Datastore is enabled."); + } + } + + /* package */ static class State { + + /* package */ static class Builder { + + // TODO(grantland): Convert mutable parameter to immutable t6941155 + public static Builder or(List> builders) { + if (builders.isEmpty()) { + throw new IllegalArgumentException("Can't take an or of an empty list of queries"); + } + + String className = null; + List constraints = new ArrayList<>(); + for (Builder builder : builders) { + if (className != null && !builder.className.equals(className)) { + throw new IllegalArgumentException( + "All of the queries in an or query must be on the same class "); + } + if (builder.limit >= 0) { + throw new IllegalArgumentException("Cannot have limits in sub queries of an 'OR' query"); + } + if (builder.skip > 0) { + throw new IllegalArgumentException("Cannot have skips in sub queries of an 'OR' query"); + } + if (!builder.order.isEmpty()) { + throw new IllegalArgumentException("Cannot have an order in sub queries of an 'OR' query"); + } + if (!builder.includes.isEmpty()) { + throw new IllegalArgumentException("Cannot have an include in sub queries of an 'OR' query"); + } + if (builder.selectedKeys != null) { + throw new IllegalArgumentException( + "Cannot have an selectKeys in sub queries of an 'OR' query"); + } + + className = builder.className; + constraints.add(builder.where); + } + + return new State.Builder(className) + .whereSatifiesAnyOf(constraints); + } + + private final String className; + private final QueryConstraints where = new QueryConstraints(); + private final Set includes = new HashSet<>(); + // This is nullable since we allow unset selectedKeys as well as no selectedKeys + private Set selectedKeys; + private int limit = -1; // negative limits mean, do not send a limit + private int skip = 0; // negative skip means do not send a skip + private List order = new ArrayList<>(); + private final Map extraOptions = new HashMap<>(); + + // TODO(grantland): Move out of State + private boolean trace; + + // Query Caching + private CachePolicy cachePolicy = CachePolicy.IGNORE_CACHE; + private long maxCacheAge = Long.MAX_VALUE; // 292 million years should be enough not to cause issues + + // LDS + private boolean isFromLocalDatastore = false; + private String pinName; + private boolean ignoreACLs; + + public Builder(String className) { + this.className = className; + } + + public Builder(Class subclass) { + this(getSubclassingController().getClassName(subclass)); + } + + public Builder(State state) { + className = state.className(); + where.putAll(state.constraints()); + includes.addAll(state.includes()); + selectedKeys = state.selectedKeys() != null ? new HashSet(state.selectedKeys()) : null; + limit = state.limit(); + skip = state.skip(); + order.addAll(state.order()); + extraOptions.putAll(state.extraOptions()); + trace = state.isTracingEnabled(); + cachePolicy = state.cachePolicy(); + maxCacheAge = state.maxCacheAge(); + isFromLocalDatastore = state.isFromLocalDatastore(); + pinName = state.pinName(); + ignoreACLs = state.ignoreACLs(); + } + + public Builder(Builder builder) { + className = builder.className; + where.putAll(builder.where); + includes.addAll(builder.includes); + selectedKeys = builder.selectedKeys != null ? new HashSet(builder.selectedKeys) : null; + limit = builder.limit; + skip = builder.skip; + order.addAll(builder.order); + extraOptions.putAll(builder.extraOptions); + trace = builder.trace; + cachePolicy = builder.cachePolicy; + maxCacheAge = builder.maxCacheAge; + isFromLocalDatastore = builder.isFromLocalDatastore; + pinName = builder.pinName; + ignoreACLs = builder.ignoreACLs; + } + + public String getClassName() { + return className; + } + + //region Where Constraints + + /** + * Add a constraint to the query that requires a particular key's value to be equal to the + * provided value. + * + * @param key + * The key to check. + * @param value + * The value that the {@link ParseObject} must contain. + * @return this, so you can chain this call. + */ + // TODO(grantland): Add typing + public Builder whereEqualTo(String key, Object value) { + where.put(key, value); + return this; + } + + // TODO(grantland): Convert mutable parameter to immutable t6941155 + public Builder whereDoesNotMatchKeyInQuery(String key, String keyInQuery, Builder builder) { + Map condition = new HashMap<>(); + condition.put("key", keyInQuery); + condition.put("query", builder); + return addConditionInternal(key, "$dontSelect", Collections.unmodifiableMap(condition)); + } + + // TODO(grantland): Convert mutable parameter to immutable t6941155 + public Builder whereMatchesKeyInQuery(String key, String keyInQuery, Builder builder) { + Map condition = new HashMap<>(); + condition.put("key", keyInQuery); + condition.put("query", builder); + return addConditionInternal(key, "$select", Collections.unmodifiableMap(new HashMap<>(condition))); + } + + // TODO(grantland): Convert mutable parameter to immutable t6941155 + public Builder whereDoesNotMatchQuery(String key, Builder builder) { + return addConditionInternal(key, "$notInQuery", builder); + } + + // TODO(grantland): Convert mutable parameter to immutable t6941155 + public Builder whereMatchesQuery(String key, Builder builder) { + return addConditionInternal(key, "$inQuery", builder); + } + + public Builder whereNear(String key, ParseGeoPoint point) { + return addCondition(key, "$nearSphere", point); + } + + public Builder maxDistance(String key, double maxDistance) { + return addCondition(key, "$maxDistance", maxDistance); + } + + public Builder whereWithin(String key, ParseGeoPoint southwest, ParseGeoPoint northeast) { + List array = new ArrayList<>(); + array.add(southwest); + array.add(northeast); + Map> dictionary = new HashMap<>(); + dictionary.put("$box", array); + return addCondition(key, "$within", dictionary); + } + + public Builder whereGeoWithin(String key, List points) { + Map> dictionary = new HashMap<>(); + dictionary.put("$polygon", points); + return addCondition(key, "$geoWithin", dictionary); + } + + public Builder whereGeoIntersects(String key, ParseGeoPoint point) { + Map dictionary = new HashMap<>(); + dictionary.put("$point", point); + return addCondition(key, "$geoIntersects", dictionary); + } + + public Builder whereText(String key, String value) { + Map termDictionary = new HashMap<>(); + Map> searchDictionary = new HashMap<>(); + termDictionary.put("$term", value); + searchDictionary.put("$search", termDictionary); + return addCondition(key, "$text", searchDictionary); + } + + public Builder addCondition(String key, String condition, + Collection value) { + return addConditionInternal(key, condition, Collections.unmodifiableCollection(value)); + } + + // TODO(grantland): Add typing + public Builder addCondition(String key, String condition, Object value) { + return addConditionInternal(key, condition, value); + } + + // Helper for condition queries. + private Builder addConditionInternal(String key, String condition, Object value) { + KeyConstraints whereValue = null; + + // Check if we already have some of a condition + if (where.containsKey(key)) { + Object existingValue = where.get(key); + if (existingValue instanceof KeyConstraints) { + whereValue = (KeyConstraints) existingValue; + } + } + if (whereValue == null) { + whereValue = new KeyConstraints(); + } + + whereValue.put(condition, value); + + where.put(key, whereValue); + return this; + } + + // Used by ParseRelation + /* package */ Builder whereRelatedTo(ParseObject parent, String key) { + where.put("$relatedTo", new RelationConstraint(key, parent)); + return this; + } + + /** + * Add a constraint that a require matches any one of an array of {@code ParseQuery}s. + *

+ * The {@code ParseQuery}s passed cannot have any orders, skips, or limits set. + * + * @param constraints + * The array of queries to or + * + * @return this, so you can chain this call. + */ + private Builder whereSatifiesAnyOf(List constraints) { + where.put("$or", constraints); + return this; + } + + // Used by getInBackground + /* package */ Builder whereObjectIdEquals(String objectId) { + where.clear(); + where.put("objectId", objectId); + return this; + } + + // Used by clear + /* package */ Builder clear(String key) { + where.remove(key); + return this; + } + + //endregion + + //region Order + + private Builder setOrder(String key) { + order.clear(); + order.add(key); + return this; + } + + private Builder addOrder(String key) { + order.add(key); + return this; + } + + /** + * Sorts the results in ascending order by the given key. + * + * @param key + * The key to order by. + * @return this, so you can chain this call. + */ + public Builder orderByAscending(String key) { + return setOrder(key); + } + + /** + * Also sorts the results in ascending order by the given key. + *

+ * The previous sort keys have precedence over this key. + * + * @param key + * The key to order by + * @return this, so you can chain this call. + */ + public Builder addAscendingOrder(String key) { + return addOrder(key); + } + + /** + * Sorts the results in descending order by the given key. + * + * @param key + * The key to order by. + * @return this, so you can chain this call. + */ + public Builder orderByDescending(String key) { + return setOrder(String.format("-%s", key)); + } + + /** + * Also sorts the results in descending order by the given key. + *

+ * The previous sort keys have precedence over this key. + * + * @param key + * The key to order by + * @return this, so you can chain this call. + */ + public Builder addDescendingOrder(String key) { + return addOrder(String.format("-%s", key)); + } + + //endregion + + //region Includes + + /** + * Include nested {@link ParseObject}s for the provided key. + *

+ * You can use dot notation to specify which fields in the included object that are also fetched. + * + * @param key + * The key that should be included. + * @return this, so you can chain this call. + */ + public Builder include(String key) { + includes.add(key); + return this; + } + + //endregion + + /** + * Restrict the fields of returned {@link ParseObject}s to only include the provided keys. + *

+ * If this is called multiple times, then all of the keys specified in each of the calls will be + * included. + *

+ * Note: This option will be ignored when querying from the local datastore. This + * is done since all the keys will be in memory anyway and there will be no performance gain from + * removing them. + * + * @param keys + * The set of keys to include in the result. + * @return this, so you can chain this call. + */ + public Builder selectKeys(Collection keys) { + if (selectedKeys == null) { + selectedKeys = new HashSet<>(); + } + selectedKeys.addAll(keys); + return this; + } + + public int getLimit() { + return limit; + } + + public Builder setLimit(int limit) { + this.limit = limit; + return this; + } + + public int getSkip() { + return skip; + } + + public Builder setSkip(int skip) { + this.skip = skip; + return this; + } + + // Used by ParseRelation + /* package */ Builder redirectClassNameForKey(String key) { + extraOptions.put("redirectClassNameForKey", key); + return this; + } + + public Builder setTracingEnabled(boolean trace) { + this.trace = trace; + return this; + } + + public CachePolicy getCachePolicy() { + throwIfLDSEnabled(); + return cachePolicy; + } + + public Builder setCachePolicy(CachePolicy cachePolicy) { + throwIfLDSEnabled(); + this.cachePolicy = cachePolicy; + return this; + } + + public long getMaxCacheAge() { + throwIfLDSEnabled(); + return maxCacheAge; + } + + public Builder setMaxCacheAge(long maxCacheAge) { + throwIfLDSEnabled(); + this.maxCacheAge = maxCacheAge; + return this; + } + + public boolean isFromNetwork() { + throwIfLDSDisabled(); + return !isFromLocalDatastore; + } + + public Builder fromNetwork() { + throwIfLDSDisabled(); + isFromLocalDatastore = false; + pinName = null; + return this; + } + + public Builder fromLocalDatastore() { + return fromPin(null); + } + + public boolean isFromLocalDatstore() { + return isFromLocalDatastore; + } + + public Builder fromPin() { + return fromPin(ParseObject.DEFAULT_PIN); + } + + public Builder fromPin(String pinName) { + throwIfLDSDisabled(); + isFromLocalDatastore = true; + this.pinName = pinName; + return this; + } + + public Builder ignoreACLs() { + throwIfLDSDisabled(); + ignoreACLs = true; + return this; + } + + public State build() { + if (!isFromLocalDatastore && ignoreACLs) { + throw new IllegalStateException("`ignoreACLs` cannot be combined with network queries"); + } + return new State<>(this); + } + } + + private final String className; + private final QueryConstraints where; + private final Set include; + private final Set selectedKeys; + private final int limit; + private final int skip; + private final List order; + private final Map extraOptions; + + // TODO(grantland): Move out of State + private final boolean trace; + + // Query Caching + private final CachePolicy cachePolicy; + private final long maxCacheAge; + + // LDS + private final boolean isFromLocalDatastore; + private final String pinName; + private final boolean ignoreACLs; + + private State(Builder builder) { + className = builder.className; + where = new QueryConstraints(builder.where); + include = Collections.unmodifiableSet(new HashSet<>(builder.includes)); + selectedKeys = builder.selectedKeys != null + ? Collections.unmodifiableSet(new HashSet<>(builder.selectedKeys)) + : null; + limit = builder.limit; + skip = builder.skip; + order = Collections.unmodifiableList(new ArrayList<>(builder.order)); + extraOptions = Collections.unmodifiableMap(new HashMap<>(builder.extraOptions)); + + trace = builder.trace; + + cachePolicy = builder.cachePolicy; + maxCacheAge = builder.maxCacheAge; + + isFromLocalDatastore = builder.isFromLocalDatastore; + pinName = builder.pinName; + ignoreACLs = builder.ignoreACLs; + } + + public String className() { + return className; + } + + public QueryConstraints constraints() { + return where; + } + + public Set includes() { + return include; + } + + public Set selectedKeys() { + return selectedKeys; + } + + public int limit() { + return limit; + } + + public int skip() { + return skip; + } + + public List order() { + return order; + } + + public Map extraOptions() { + return extraOptions; + } + + public boolean isTracingEnabled() { + return trace; + } + + public CachePolicy cachePolicy() { + return cachePolicy; + } + + public long maxCacheAge() { + return maxCacheAge; + } + + public boolean isFromLocalDatastore() { + return isFromLocalDatastore; + } + + public String pinName() { + return pinName; + } + + public boolean ignoreACLs() { + return ignoreACLs; + } + + // Returns the query in JSON REST format for subqueries + /* package */ JSONObject toJSON(ParseEncoder encoder) { + JSONObject params = new JSONObject(); + + try { + params.put("className", className); + params.put("where", encoder.encode(where)); + + if (limit >= 0) { + params.put("limit", limit); + } + if (skip > 0) { + params.put("skip", skip); + } + if (!order.isEmpty()) { + params.put("order", ParseTextUtils.join(",", order)); + } + if (!include.isEmpty()) { + params.put("include", ParseTextUtils.join(",", include)); + } + if (selectedKeys != null) { + params.put("fields", ParseTextUtils.join(",", selectedKeys)); + } + if (trace) { + params.put("trace", 1); + } + + for (String key : extraOptions.keySet()) { + params.put(key, encoder.encode(extraOptions.get(key))); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return params; + } + + @Override + public String toString() { + return String.format(Locale.US, "%s[className=%s, where=%s, include=%s, " + + "selectedKeys=%s, limit=%s, skip=%s, order=%s, extraOptions=%s, " + + "cachePolicy=%s, maxCacheAge=%s, " + + "trace=%s]", + getClass().getName(), + className, + where, + include, + selectedKeys, + limit, + skip, + order, + extraOptions, + cachePolicy, + maxCacheAge, + trace); + } + } + + + private final State.Builder builder; + private ParseUser user; + + // Just like ParseFile + private Set> currentTasks = Collections.synchronizedSet( + new HashSet>()); + + /** + * Constructs a query for a {@link ParseObject} subclass type. A default query with no further + * parameters will retrieve all {@link ParseObject}s of the provided class. + * + * @param subclass + * The {@link ParseObject} subclass type to retrieve. + */ + public ParseQuery(Class subclass) { + this(getSubclassingController().getClassName(subclass)); + } + + /** + * Constructs a query. A default query with no further parameters will retrieve all + * {@link ParseObject}s of the provided class. + * + * @param theClassName + * The name of the class to retrieve {@link ParseObject}s for. + */ + public ParseQuery(String theClassName) { + this(new State.Builder(theClassName)); + } + + /** + * Constructs a copy of {@code query}; + * + * @param query + * The query to copy. + */ + public ParseQuery(ParseQuery query) { + this(new State.Builder<>(query.getBuilder())); + user = query.user; + } + + /* package */ ParseQuery(State.Builder builder) { + this.builder = builder; + } + + /* package */ State.Builder getBuilder() { + return builder; + } + + /** + * Sets the user to be used for this query. + * + * + * The query will use the user if set, otherwise it will read the current user. + */ + /* package for tests */ ParseQuery setUser(ParseUser user) { + this.user = user; + return this; + } + + /** + * Returns the user used for the query. This user is used to filter results based on ACLs on the + * target objects. Can be {@code null} if the there is no current user or {@link #ignoreACLs} is + * enabled. + */ + /* package for tests */ Task getUserAsync(State state) { + if (state.ignoreACLs()) { + return Task.forResult(null); + } + if (user != null) { + return Task.forResult(user); + } + return ParseUser.getCurrentUserAsync(); + } + + /** + * Cancels the current network request(s) (if any is running). + */ + //TODO (grantland): Deprecate and replace with CancellationTokens + public void cancel() { + Set> tasks = new HashSet<>(currentTasks); + for (TaskCompletionSource tcs : tasks) { + tcs.trySetCancelled(); + } + currentTasks.removeAll(tasks); + } + + public boolean isRunning() { + return currentTasks.size() > 0; + } + + /** + * Retrieves a list of {@link ParseObject}s that satisfy this query. + *

+ * @return A list of all {@link ParseObject}s obeying the conditions set in this query. + * @throws ParseException + * Throws a {@link ParseException} if no object is found. + * + * @see ParseException#OBJECT_NOT_FOUND + */ + public List find() throws ParseException { + return ParseTaskUtils.wait(findInBackground()); + } + + /** + * Retrieves at most one {@link ParseObject} that satisfies this query. + *

+ * Note:This mutates the {@code ParseQuery}. + * + * @return A {@link ParseObject} obeying the conditions set in this query. + * @throws ParseException + * Throws a {@link ParseException} if no object is found. + * + * @see ParseException#OBJECT_NOT_FOUND + */ + public T getFirst() throws ParseException { + return ParseTaskUtils.wait(getFirstInBackground()); + } + + /** + * Change the caching policy of this query. + *

+ * Unsupported when Local Datastore is enabled. + * + * @return this, so you can chain this call. + * + * @see ParseQuery#fromLocalDatastore() + * @see ParseQuery#fromPin() + * @see ParseQuery#fromPin(String) + */ + public ParseQuery setCachePolicy(CachePolicy newCachePolicy) { + builder.setCachePolicy(newCachePolicy); + return this; + } + + /** + * @return the caching policy. + */ + public CachePolicy getCachePolicy() { + return builder.getCachePolicy(); + } + + /** + * Change the source of this query to the server. + *

+ * Requires Local Datastore to be enabled. + * + * @return this, so you can chain this call. + * + * @see ParseQuery#setCachePolicy(CachePolicy) + */ + public ParseQuery fromNetwork() { + builder.fromNetwork(); + return this; + } + + /* package */ boolean isFromNetwork() { + return builder.isFromNetwork(); + } + + /** + * Change the source of this query to all pinned objects. + *

+ * Requires Local Datastore to be enabled. + * + * @return this, so you can chain this call. + * + * @see ParseQuery#setCachePolicy(CachePolicy) + */ + public ParseQuery fromLocalDatastore() { + builder.fromLocalDatastore(); + return this; + } + + /** + * Change the source of this query to the default group of pinned objects. + *

+ * Requires Local Datastore to be enabled. + * + * @return this, so you can chain this call. + * + * @see ParseObject#DEFAULT_PIN + * @see ParseQuery#setCachePolicy(CachePolicy) + */ + public ParseQuery fromPin() { + builder.fromPin(); + return this; + } + + /** + * Change the source of this query to a specific group of pinned objects. + *

+ * Requires Local Datastore to be enabled. + * + * @param name + * the pinned group + * @return this, so you can chain this call. + * + * @see ParseQuery#setCachePolicy(CachePolicy) + */ + public ParseQuery fromPin(String name) { + builder.fromPin(name); + return this; + } + + /** + * Ignore ACLs when querying from the Local Datastore. + *

+ * This is particularly useful when querying for objects with Role based ACLs set on them. + * + * @return this, so you can chain this call. + */ + public ParseQuery ignoreACLs() { + builder.ignoreACLs(); + return this; + } + + /** + * Sets the maximum age of cached data that will be considered in this query. + * + * @return this, so you can chain this call. + */ + public ParseQuery setMaxCacheAge(long maxAgeInMilliseconds) { + builder.setMaxCacheAge(maxAgeInMilliseconds); + return this; + } + + /** + * Gets the maximum age of cached data that will be considered in this query. The returned value + * is in milliseconds + */ + public long getMaxCacheAge() { + return builder.getMaxCacheAge(); + } + + + /** + * Wraps the runnable operation and keeps it in sync with the given tcs, so we know how many + * operations are running (currentTasks.size()) and can cancel them. + */ + private Task perform(Callable> runnable, final TaskCompletionSource tcs) { + currentTasks.add(tcs); + + Task task; + try { + task = runnable.call(); + } catch (Exception e) { + task = Task.forError(e); + } + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + tcs.trySetResult(null); // release + currentTasks.remove(tcs); + return task; + } + }); + } + + /** + * Retrieves a list of {@link ParseObject}s that satisfy this query from the source in a + * background thread. + *

+ * This is preferable to using {@link #find()}, unless your code is already running in a + * background thread. + * + * @return A {@link Task} that will be resolved when the find has completed. + */ + public Task> findInBackground() { + return findAsync(builder.build()); + } + + /** + * Retrieves a list of {@link ParseObject}s that satisfy this query from the source in a + * background thread. + *

+ * This is preferable to using {@link #find()}, unless your code is already running in a + * background thread. + * + * @param callback + * callback.done(objectList, e) is called when the find completes. + */ + public void findInBackground(final FindCallback callback) { + final State state = builder.build(); + + final Task> task; + if (state.cachePolicy() != CachePolicy.CACHE_THEN_NETWORK || + state.isFromLocalDatastore()) { + task = findAsync(state); + } else { + task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>>() { + @Override + public Task> call(State state, ParseUser user, Task cancellationToken) { + return findAsync(state, user, cancellationToken); + } + }); + } + ParseTaskUtils.callbackOnMainThreadAsync(task, callback); + } + + private Task> findAsync(final State state) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + return perform(new Callable>>() { + @Override + public Task> call() throws Exception { + return getUserAsync(state).onSuccessTask(new Continuation>>() { + @Override + public Task> then(Task task) throws Exception { + final ParseUser user = task.getResult(); + return findAsync(state, user, tcs.getTask()); + } + }); + } + }, tcs); + } + + /* package */ Task> findAsync(State state, ParseUser user, Task cancellationToken) { + return ParseQuery.getQueryController().findAsync(state, user, cancellationToken); + } + + /** + * Retrieves at most one {@link ParseObject} that satisfies this query from the source in a + * background thread. + *

+ * This is preferable to using {@link #getFirst()}, unless your code is already running in a + * background thread. + *

+ * Note:This mutates the {@code ParseQuery}. + * + * @return A {@link Task} that will be resolved when the get has completed. + */ + public Task getFirstInBackground() { + final State state = builder.setLimit(1) + .build(); + return getFirstAsync(state); + } + + /** + * Retrieves at most one {@link ParseObject} that satisfies this query from the source in a + * background thread. + *

+ * This is preferable to using {@link #getFirst()}, unless your code is already running in a + * background thread. + *

+ * Note:This mutates the {@code ParseQuery}. + * + * @param callback + * callback.done(object, e) is called when the find completes. + */ + public void getFirstInBackground(final GetCallback callback) { + final State state = builder.setLimit(1) + .build(); + + final Task task; + if (state.cachePolicy() != CachePolicy.CACHE_THEN_NETWORK || + state.isFromLocalDatastore()) { + task = getFirstAsync(state); + } else { + task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>() { + @Override + public Task call(State state, ParseUser user, Task cancellationToken) { + return getFirstAsync(state, user, cancellationToken); + } + }); + } + ParseTaskUtils.callbackOnMainThreadAsync(task, callback); + } + + private Task getFirstAsync(final State state) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + return perform(new Callable>() { + @Override + public Task call() throws Exception { + return getUserAsync(state).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser user = task.getResult(); + return getFirstAsync(state, user, tcs.getTask()); + } + }); + } + }, tcs); + } + + private Task getFirstAsync(State state, ParseUser user, Task cancellationToken) { + return ParseQuery.getQueryController().getFirstAsync(state, user, cancellationToken); + } + + /** + * Counts the number of objects that match this query. This does not use caching. + * + * @throws ParseException + * Throws an exception when the network connection fails or when the query is invalid. + */ + public int count() throws ParseException { + return ParseTaskUtils.wait(countInBackground()); + } + + /** + * Counts the number of objects that match this query in a background thread. This does not use + * caching. + * + * @return A {@link Task} that will be resolved when the count has completed. + */ + public Task countInBackground() { + State.Builder copy = new State.Builder<>(builder); + final State state = copy.setLimit(0).build(); + return countAsync(state); + } + + /** + * Counts the number of objects that match this query in a background thread. This does not use + * caching. + * + * @param callback + * callback.done(count, e) will be called when the count completes. + */ + public void countInBackground(final CountCallback callback) { + State.Builder copy = new State.Builder<>(builder); + final State state = copy.setLimit(0).build(); + + // Hack to workaround CountCallback's non-uniform signature. + final ParseCallback2 c = callback != null + ? new ParseCallback2() { + @Override + public void done(Integer integer, ParseException e) { + callback.done(e == null ? integer : -1, e); + } + } + : null; + + final Task task; + if (state.cachePolicy() != CachePolicy.CACHE_THEN_NETWORK || + state.isFromLocalDatastore()) { + task = countAsync(state); + } else { + task = doCacheThenNetwork(state, c, new CacheThenNetworkCallable>() { + @Override + public Task call(State state, ParseUser user, Task cancellationToken) { + return countAsync(state, user, cancellationToken); + } + }); + } + ParseTaskUtils.callbackOnMainThreadAsync(task, c); + } + + private Task countAsync(final State state) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + return perform(new Callable>() { + @Override + public Task call() throws Exception { + return getUserAsync(state).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser user = task.getResult(); + return countAsync(state, user, tcs.getTask()); + } + }); + } + }, tcs); + } + + private Task countAsync(State state, ParseUser user, Task cancellationToken) { + return ParseQuery.getQueryController().countAsync(state, user, cancellationToken); + } + + /** + * Constructs a {@link ParseObject} whose id is already known by fetching data from the source. + *

+ * Note:This mutates the {@code ParseQuery}. + * + * @param objectId + * Object id of the {@link ParseObject} to fetch. + * @throws ParseException + * Throws an exception when there is no such object or when the network connection + * fails. + * + * @see ParseException#OBJECT_NOT_FOUND + */ + public T get(final String objectId) throws ParseException { + return ParseTaskUtils.wait(getInBackground(objectId)); + } + + /** + * Returns whether or not this query has a cached result. + */ + //TODO (grantland): should be done Async since it does disk i/o & calls through to current user + public boolean hasCachedResult() { + throwIfLDSEnabled(); + + // TODO(grantland): Is there a more efficient way to accomplish this rather than building a + // new state just to check it's cacheKey? + State state = builder.build(); + + ParseUser user = null; + try { + user = ParseTaskUtils.wait(getUserAsync(state)); + } catch (ParseException e) { + // do nothing + } + String sessionToken = user != null ? user.getSessionToken() : null; + + /* + * TODO: Once the count queries are cached, only return false when both queries miss in the + * cache. + */ + String raw = ParseKeyValueCache.loadFromKeyValueCache( + ParseRESTQueryCommand.findCommand(state, sessionToken).getCacheKey(), state.maxCacheAge() + ); + return raw != null; + } + + /** + * Removes the previously cached result for this query, forcing the next find() to hit the + * network. If there is no cached result for this query, then this is a no-op. + */ + //TODO (grantland): should be done Async since it does disk i/o & calls through to current user + public void clearCachedResult() { + throwIfLDSEnabled(); + + // TODO(grantland): Is there a more efficient way to accomplish this rather than building a + // new state just to check it's cacheKey? + State state = builder.build(); + + ParseUser user = null; + try { + user = ParseTaskUtils.wait(getUserAsync(state)); + } catch (ParseException e) { + // do nothing + } + String sessionToken = user != null ? user.getSessionToken() : null; + + // TODO: Once the count queries are cached, handle the cached results of the count query. + ParseKeyValueCache.clearFromKeyValueCache( + ParseRESTQueryCommand.findCommand(state, sessionToken).getCacheKey() + ); + } + + /** + * Clears the cached result for all queries. + */ + public static void clearAllCachedResults() { + throwIfLDSEnabled(); + + ParseKeyValueCache.clearKeyValueCacheDir(); + } + + /** + * Constructs a {@link ParseObject} whose id is already known by fetching data from the source in a + * background thread. This does not use caching. + *

+ * This is preferable to using the {@link ParseObject#createWithoutData(String, String)}, unless + * your code is already running in a background thread. + * + * @param objectId + * Object id of the {@link ParseObject} to fetch. + * + * @return A {@link Task} that is resolved when the fetch completes. + */ + // TODO(grantland): Why is this an instance method? Shouldn't this just be a static method since + // other parameters don't even make sense here? + // We'll need to add a version with CancellationToken if we do. + public Task getInBackground(final String objectId) { + final State state = builder.setSkip(-1) + .whereObjectIdEquals(objectId) + .build(); + return getFirstAsync(state); + } + + /** + * Constructs a {@link ParseObject} whose id is already known by fetching data from the source in + * a background thread. This does not use caching. + *

+ * This is preferable to using the {@link ParseObject#createWithoutData(String, String)}, unless + * your code is already running in a background thread. + * + * @param objectId + * Object id of the {@link ParseObject} to fetch. + * @param callback + * callback.done(object, e) will be called when the fetch completes. + */ + // TODO(grantland): Why is this an instance method? Shouldn't this just be a static method since + // other parameters don't even make sense here? + // We'll need to add a version with CancellationToken if we do. + public void getInBackground(final String objectId, final GetCallback callback) { + final State state = builder.setSkip(-1) + .whereObjectIdEquals(objectId) + .build(); + + final Task task; + if (state.cachePolicy() != CachePolicy.CACHE_THEN_NETWORK || + state.isFromLocalDatastore()) { + task = getFirstAsync(state); + } else { + task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>() { + @Override + public Task call(State state, ParseUser user, Task cancellationToken) { + return getFirstAsync(state, user, cancellationToken); + } + }); + } + ParseTaskUtils.callbackOnMainThreadAsync(task, callback); + } + + //region CACHE_THEN_NETWORK + + /** + * Helper method for CACHE_THEN_NETWORK. + * + * Serially executes the {@code delegate} once in cache with the {@code} callback and then returns + * a task for the execution of the second {@code delegate} execution on the network for the caller + * to callback on. + */ + private Task doCacheThenNetwork( + final ParseQuery.State state, + final ParseCallback2 callback, + final CacheThenNetworkCallable> delegate) { + + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + return perform(new Callable>() { + @Override + public Task call() throws Exception { + return getUserAsync(state).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser user = task.getResult(); + final State cacheState = new State.Builder(state) + .setCachePolicy(CachePolicy.CACHE_ONLY) + .build(); + final State networkState = new State.Builder(state) + .setCachePolicy(CachePolicy.NETWORK_ONLY) + .build(); + + Task executionTask = delegate.call(cacheState, user, tcs.getTask()); + executionTask = ParseTaskUtils.callbackOnMainThreadAsync(executionTask, callback); + return executionTask.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isCancelled()) { + return task; + } + return delegate.call(networkState, user, tcs.getTask()); + } + }); + } + }); + } + }, tcs); + } + + private interface CacheThenNetworkCallable { + TResult call(ParseQuery.State state, ParseUser user, Task cancellationToken); + } + + //endregion + + /** + * Add a constraint to the query that requires a particular key's value to be equal to the + * provided value. + * + * @param key + * The key to check. + * @param value + * The value that the {@link ParseObject} must contain. + * @return this, so you can chain this call. + */ + public ParseQuery whereEqualTo(String key, Object value) { + builder.whereEqualTo(key, value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be less than the + * provided value. + * + * @param key + * The key to check. + * @param value + * The value that provides an upper bound. + * @return this, so you can chain this call. + */ + public ParseQuery whereLessThan(String key, Object value) { + builder.addCondition(key, "$lt", value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be not equal to the + * provided value. + * + * @param key + * The key to check. + * @param value + * The value that must not be equalled. + * @return this, so you can chain this call. + */ + public ParseQuery whereNotEqualTo(String key, Object value) { + builder.addCondition(key, "$ne", value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be greater than the + * provided value. + * + * @param key + * The key to check. + * @param value + * The value that provides an lower bound. + * @return this, so you can chain this call. + */ + public ParseQuery whereGreaterThan(String key, Object value) { + builder.addCondition(key, "$gt", value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be less than or equal + * to the provided value. + * + * @param key + * The key to check. + * @param value + * The value that provides an upper bound. + * @return this, so you can chain this call. + */ + public ParseQuery whereLessThanOrEqualTo(String key, Object value) { + builder.addCondition(key, "$lte", value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be greater than or + * equal to the provided value. + * + * @param key + * The key to check. + * @param value + * The value that provides an lower bound. + * @return this, so you can chain this call. + */ + public ParseQuery whereGreaterThanOrEqualTo(String key, Object value) { + builder.addCondition(key, "$gte", value); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value to be contained in the + * provided list of values. + * + * @param key + * The key to check. + * @param values + * The values that will match. + * @return this, so you can chain this call. + */ + public ParseQuery whereContainedIn(String key, Collection values) { + builder.addCondition(key, "$in", values); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value match another + * {@code ParseQuery}. + *

+ * This only works on keys whose values are {@link ParseObject}s or lists of {@link ParseObject}s. + * Add a constraint to the query that requires a particular key's value to contain every one of + * the provided list of values. + * + * @param key + * The key to check. This key's value must be an array. + * @param values + * The values that will match. + * @return this, so you can chain this call. + */ + public ParseQuery whereContainsAll(String key, Collection values) { + builder.addCondition(key, "$all", values); + return this; + } + + /** + * Adds a constraint for finding string values that contain a provided + * string using Full Text Search + * + * Requires Parse-Server@2.5.0 + * + * @param key + * The key to be constrained. + * @param text + * String to be searched + * @return this, so you can chain this call. + */ + public ParseQuery whereFullText(String key, String text) { + builder.whereText(key, text); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value match another + * {@code ParseQuery}. + *

+ * This only works on keys whose values are {@link ParseObject}s or lists of {@link ParseObject}s. + * + * @param key + * The key to check. + * @param query + * The query that the value should match + * @return this, so you can chain this call. + */ + public ParseQuery whereMatchesQuery(String key, ParseQuery query) { + builder.whereMatchesQuery(key, query.getBuilder()); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value does not match another + * {@code ParseQuery}. + *

+ * This only works on keys whose values are {@link ParseObject}s or lists of {@link ParseObject}s. + * + * @param key + * The key to check. + * @param query + * The query that the value should not match + * @return this, so you can chain this call. + */ + public ParseQuery whereDoesNotMatchQuery(String key, ParseQuery query) { + builder.whereDoesNotMatchQuery(key, query.getBuilder()); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value matches a value for a key + * in the results of another {@code ParseQuery}. + * + * @param key + * The key whose value is being checked + * @param keyInQuery + * The key in the objects from the sub query to look in + * @param query + * The sub query to run + * @return this, so you can chain this call. + */ + public ParseQuery whereMatchesKeyInQuery(String key, String keyInQuery, ParseQuery query) { + builder.whereMatchesKeyInQuery(key, keyInQuery, query.getBuilder()); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value does not match any value + * for a key in the results of another {@code ParseQuery}. + * + * @param key + * The key whose value is being checked and excluded + * @param keyInQuery + * The key in the objects from the sub query to look in + * @param query + * The sub query to run + * @return this, so you can chain this call. + */ + public ParseQuery whereDoesNotMatchKeyInQuery(String key, String keyInQuery, + ParseQuery query) { + builder.whereDoesNotMatchKeyInQuery(key, keyInQuery, query.getBuilder()); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's value not be contained in the + * provided list of values. + * + * @param key + * The key to check. + * @param values + * The values that will not match. + * @return this, so you can chain this call. + */ + public ParseQuery whereNotContainedIn(String key, Collection values) { + builder.addCondition(key, "$nin", values); + return this; + } + + /** + * Add a proximity based constraint for finding objects with key point values near the point + * given. + * + * @param key + * The key that the {@link ParseGeoPoint} is stored in. + * @param point + * The reference {@link ParseGeoPoint} that is used. + * @return this, so you can chain this call. + */ + public ParseQuery whereNear(String key, ParseGeoPoint point) { + builder.whereNear(key, point); + return this; + } + + /** + * Add a proximity based constraint for finding objects with key point values near the point given + * and within the maximum distance given. + *

+ * Radius of earth used is {@code 3958.8} miles. + * + * @param key + * The key that the {@link ParseGeoPoint} is stored in. + * @param point + * The reference {@link ParseGeoPoint} that is used. + * @param maxDistance + * Maximum distance (in miles) of results to return. + * @return this, so you can chain this call. + */ + public ParseQuery whereWithinMiles(String key, ParseGeoPoint point, double maxDistance) { + return whereWithinRadians(key, point, maxDistance / ParseGeoPoint.EARTH_MEAN_RADIUS_MILE); + } + + /** + * Add a proximity based constraint for finding objects with key point values near the point given + * and within the maximum distance given. + *

+ * Radius of earth used is {@code 6371.0} kilometers. + * + * @param key + * The key that the {@link ParseGeoPoint} is stored in. + * @param point + * The reference {@link ParseGeoPoint} that is used. + * @param maxDistance + * Maximum distance (in kilometers) of results to return. + * @return this, so you can chain this call. + */ + public ParseQuery whereWithinKilometers(String key, ParseGeoPoint point, double maxDistance) { + return whereWithinRadians(key, point, maxDistance / ParseGeoPoint.EARTH_MEAN_RADIUS_KM); + } + + /** + * Add a proximity based constraint for finding objects with key point values near the point given + * and within the maximum distance given. + * + * @param key + * The key that the {@link ParseGeoPoint} is stored in. + * @param point + * The reference {@link ParseGeoPoint} that is used. + * @param maxDistance + * Maximum distance (in radians) of results to return. + * @return this, so you can chain this call. + */ + public ParseQuery whereWithinRadians(String key, ParseGeoPoint point, double maxDistance) { + builder.whereNear(key, point) + .maxDistance(key, maxDistance); + return this; + } + + /** + * Add a constraint to the query that requires a particular key's coordinates be contained within + * a given rectangular geographic bounding box. + * + * @param key + * The key to be constrained. + * @param southwest + * The lower-left inclusive corner of the box. + * @param northeast + * The upper-right inclusive corner of the box. + * @return this, so you can chain this call. + */ + public ParseQuery whereWithinGeoBox( + String key, ParseGeoPoint southwest, ParseGeoPoint northeast) { + builder.whereWithin(key, southwest, northeast); + return this; + } + + /** + * Adds a constraint to the query that requires a particular key's + * coordinates be contained within and on the bounds of a given polygon. + * Supports closed and open (last point is connected to first) paths + * + * Polygon must have at least 3 points + * + * @param key + * The key to be constrained. + * @param value + * List or ParsePolygon + * @return this, so you can chain this call. + */ + public ParseQuery whereWithinPolygon(String key, List points) { + builder.whereGeoWithin(key, points); + return this; + } + + public ParseQuery whereWithinPolygon(String key, ParsePolygon polygon) { + return whereWithinPolygon(key, polygon.getCoordinates()); + } + + /** + * Add a constraint to the query that requires a particular key's + * coordinates that contains a {@link ParseGeoPoint}s + * + * (Requires parse-server@2.6.0) + * + * @param key + * The key to be constrained. + * @param point + * ParseGeoPoint + * @return this, so you can chain this call. + */ + public ParseQuery wherePolygonContains(String key, ParseGeoPoint point) { + builder.whereGeoIntersects(key, point); + return this; + } + + /** + * Add a regular expression constraint for finding string values that match the provided regular + * expression. + *

+ * This may be slow for large datasets. + * + * @param key + * The key that the string to match is stored in. + * @param regex + * The regular expression pattern to match. + * @return this, so you can chain this call. + */ + public ParseQuery whereMatches(String key, String regex) { + builder.addCondition(key, "$regex", regex); + return this; + } + + /** + * Add a regular expression constraint for finding string values that match the provided regular + * expression. + *

+ * This may be slow for large datasets. + * + * @param key + * The key that the string to match is stored in. + * @param regex + * The regular expression pattern to match. + * @param modifiers + * Any of the following supported PCRE modifiers:
+ * i - Case insensitive search
+ * m - Search across multiple lines of input
+ * @return this, so you can chain this call. + */ + public ParseQuery whereMatches(String key, String regex, String modifiers) { + builder.addCondition(key, "$regex", regex); + if (modifiers.length() != 0) { + builder.addCondition(key, "$options", modifiers); + } + return this; + } + + /** + * Add a constraint for finding string values that contain a provided string. + *

+ * This will be slow for large datasets. + * + * @param key + * The key that the string to match is stored in. + * @param substring + * The substring that the value must contain. + * @return this, so you can chain this call. + */ + public ParseQuery whereContains(String key, String substring) { + String regex = Pattern.quote(substring); + whereMatches(key, regex); + return this; + } + + /** + * Add a constraint for finding string values that start with a provided string. + *

+ * This query will use the backend index, so it will be fast even for large datasets. + * + * @param key + * The key that the string to match is stored in. + * @param prefix + * The substring that the value must start with. + * @return this, so you can chain this call. + */ + public ParseQuery whereStartsWith(String key, String prefix) { + String regex = "^" + Pattern.quote(prefix); + whereMatches(key, regex); + return this; + } + + /** + * Add a constraint for finding string values that end with a provided string. + *

+ * This will be slow for large datasets. + * + * @param key + * The key that the string to match is stored in. + * @param suffix + * The substring that the value must end with. + * @return this, so you can chain this call. + */ + public ParseQuery whereEndsWith(String key, String suffix) { + String regex = Pattern.quote(suffix) + "$"; + whereMatches(key, regex); + return this; + } + + /** + * Include nested {@link ParseObject}s for the provided key. + *

+ * You can use dot notation to specify which fields in the included object that are also fetched. + * + * @param key + * The key that should be included. + * @return this, so you can chain this call. + */ + public ParseQuery include(String key) { + builder.include(key); + return this; + } + + /** + * Restrict the fields of returned {@link ParseObject}s to only include the provided keys. + *

+ * If this is called multiple times, then all of the keys specified in each of the calls will be + * included. + *

+ * Note: This option will be ignored when querying from the local datastore. This + * is done since all the keys will be in memory anyway and there will be no performance gain from + * removing them. + * + * @param keys + * The set of keys to include in the result. + * @return this, so you can chain this call. + */ + public ParseQuery selectKeys(Collection keys) { + builder.selectKeys(keys); + return this; + } + + /** + * Add a constraint for finding objects that contain the given key. + * + * @param key + * The key that should exist. + * + * @return this, so you can chain this call. + */ + public ParseQuery whereExists(String key) { + builder.addCondition(key, "$exists", true); + return this; + } + + /** + * Add a constraint for finding objects that do not contain a given key. + * + * @param key + * The key that should not exist + * + * @return this, so you can chain this call. + */ + public ParseQuery whereDoesNotExist(String key) { + builder.addCondition(key, "$exists", false); + return this; + } + + /** + * Sorts the results in ascending order by the given key. + * + * @param key + * The key to order by. + * @return this, so you can chain this call. + */ + public ParseQuery orderByAscending(String key) { + builder.orderByAscending(key); + return this; + } + + /** + * Also sorts the results in ascending order by the given key. + *

+ * The previous sort keys have precedence over this key. + * + * @param key + * The key to order by + * @return this, so you can chain this call. + */ + public ParseQuery addAscendingOrder(String key) { + builder.addAscendingOrder(key); + return this; + } + + /** + * Sorts the results in descending order by the given key. + * + * @param key + * The key to order by. + * @return this, so you can chain this call. + */ + public ParseQuery orderByDescending(String key) { + builder.orderByDescending(key); + return this; + } + + /** + * Also sorts the results in descending order by the given key. + *

+ * The previous sort keys have precedence over this key. + * + * @param key + * The key to order by + * @return this, so you can chain this call. + */ + public ParseQuery addDescendingOrder(String key) { + builder.addDescendingOrder(key); + return this; + } + + /** + * Controls the maximum number of results that are returned. + *

+ * Setting a negative limit denotes retrieval without a limit. The default limit is {@code 100}, + * with a maximum of {@code 1000} results being returned at a time. + * + * @param newLimit The new limit. + * @return this, so you can chain this call. + */ + public ParseQuery setLimit(int newLimit) { + builder.setLimit(newLimit); + return this; + } + + /** + * Accessor for the limit. + */ + public int getLimit() { + return builder.getLimit(); + } + + /** + * Controls the number of results to skip before returning any results. + *

+ * This is useful for pagination. Default is to skip zero results. + * + * @param newSkip The new skip + * @return this, so you can chain this call. + */ + public ParseQuery setSkip(int newSkip) { + builder.setSkip(newSkip); + return this; + } + + /** + * Accessor for the skip value. + */ + public int getSkip() { + return builder.getSkip(); + } + + /** + * Accessor for the class name. + */ + public String getClassName() { + return builder.getClassName(); + } + + /** + * Clears constraints related to the given key, if any was set previously. + * Order, includes and selected keys are not affected by this operation. + * + * @param key key to be cleared from current constraints. + * @return this, so you can chain this call. + */ + public ParseQuery clear(String key) { + builder.clear(key); + return this; + } + + /** + * Turn on performance tracing of finds. + *

+ * If performance tracing is already turned on this does nothing. In general you don't need to call trace. + * + * @return this, so you can chain this call. + */ + public ParseQuery setTrace(boolean shouldTrace) { + builder.setTracingEnabled(shouldTrace); + return this; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQueryController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQueryController.java new file mode 100644 index 0000000..3b5849c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseQueryController.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +import bolts.Task; + +/** + * A {@code ParseQueryController} defines how a {@link ParseQuery} is executed. + */ +/** package */ interface ParseQueryController { + + /** + * Executor for {@code find} queries. + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A {@link Task} that resolves to the results of the find. + */ + Task> findAsync(ParseQuery.State state, ParseUser user, + Task cancellationToken); + + /** + * Executor for {@code count} queries. + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A {@link Task} that resolves to the results of the count. + */ + Task countAsync(ParseQuery.State state, ParseUser user, + Task cancellationToken); + + /** + * Executor for {@code getFirst} queries. + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A {@link Task} that resolves to the the first result of the query if successful and + * there is at least one result or {@link ParseException#OBJECT_NOT_FOUND} if there are no + * results. + */ + Task getFirstAsync(ParseQuery.State state, ParseUser user, + Task cancellationToken); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTAnalyticsCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTAnalyticsCommand.java new file mode 100644 index 0000000..bf1dfc1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTAnalyticsCommand.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.net.Uri; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; +import java.util.Map; + +/** package */ class ParseRESTAnalyticsCommand extends ParseRESTCommand { + + /** + * The set of predefined events + */ + // Tracks the AppOpened event + /* package for test */ static final String EVENT_APP_OPENED = "AppOpened"; + + private static final String PATH = "events/%s"; + + private static final String KEY_AT = "at"; + private static final String KEY_PUSH_HASH = "push_hash"; + private static final String KEY_DIMENSIONS = "dimensions"; + + public static ParseRESTAnalyticsCommand trackAppOpenedCommand( + String pushHash, String sessionToken) { + return trackEventCommand(EVENT_APP_OPENED, pushHash, null, sessionToken); + } + + public static ParseRESTAnalyticsCommand trackEventCommand( + String eventName, Map dimensions, String sessionToken) { + return trackEventCommand(eventName, null, dimensions, sessionToken); + } + + /* package */ static ParseRESTAnalyticsCommand trackEventCommand( + String eventName, String pushHash, Map dimensions, String sessionToken) { + String httpPath = String.format(PATH, Uri.encode(eventName)); + JSONObject parameters = new JSONObject(); + try { + parameters.put(KEY_AT, NoObjectsEncoder.get().encode(new Date())); + if (pushHash != null) { + parameters.put(KEY_PUSH_HASH, pushHash); + } + if (dimensions != null) { + parameters.put(KEY_DIMENSIONS, NoObjectsEncoder.get().encode(dimensions)); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return new ParseRESTAnalyticsCommand( + httpPath, ParseHttpRequest.Method.POST, parameters, sessionToken); + } + + public ParseRESTAnalyticsCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCloudCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCloudCommand.java new file mode 100644 index 0000000..6439d05 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCloudCommand.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import java.util.Map; + +/** package */ class ParseRESTCloudCommand extends ParseRESTCommand { + + private ParseRESTCloudCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } + + public static ParseRESTCloudCommand callFunctionCommand(String functionName, + Map parameters, String sessionToken) { + final String httpPath = String.format("functions/%s", functionName); + return new ParseRESTCloudCommand( + httpPath, ParseHttpRequest.Method.POST, parameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCommand.java new file mode 100644 index 0000000..553deef --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTCommand.java @@ -0,0 +1,539 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONStringer; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import bolts.Task; + +/** + * A helper object to send requests to the server. + */ +/** package */ class ParseRESTCommand extends ParseRequest { + + /* package */ static final String HEADER_APPLICATION_ID = "X-Parse-Application-Id"; + /* package */ static final String HEADER_CLIENT_KEY = "X-Parse-Client-Key"; + /* package */ static final String HEADER_CLIENT_VERSION = "X-Parse-Client-Version"; + /* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version"; + /* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version"; + /* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version"; + + /* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id"; + /* package */ static final String USER_AGENT = "User-Agent"; + private static final String HEADER_SESSION_TOKEN = "X-Parse-Session-Token"; + private static final String HEADER_MASTER_KEY = "X-Parse-Master-Key"; + private static final String PARAMETER_METHOD_OVERRIDE = "_method"; + + // Set via Parse.initialize(Configuration) + /* package */ static URL server = null; + + private static LocalIdManager getLocalIdManager() { + return ParseCorePlugins.getInstance().getLocalIdManager(); + } + + /* package */ static abstract class Init> { + private String sessionToken; + private String installationId; + public String masterKey; + + private ParseHttpRequest.Method method = ParseHttpRequest.Method.GET; + private String httpPath; + private JSONObject jsonParameters; + + private String operationSetUUID; + private String localId; + + /* package */ abstract T self(); + + public T sessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return self(); + } + + public T installationId(String installationId) { + this.installationId = installationId; + return self(); + } + + public T masterKey(String masterKey) { + this.masterKey = masterKey; + return self(); + } + + public T method(ParseHttpRequest.Method method) { + this.method = method; + return self(); + } + + public T httpPath(String httpPath) { + this.httpPath = httpPath; + return self(); + } + + public T jsonParameters(JSONObject jsonParameters) { + this.jsonParameters = jsonParameters; + return self(); + } + + public T operationSetUUID(String operationSetUUID) { + this.operationSetUUID = operationSetUUID; + return self(); + } + + public T localId(String localId) { + this.localId = localId; + return self(); + } + } + + public static class Builder extends Init { + @Override + /* package */ Builder self() { + return this; + } + + public ParseRESTCommand build() { + return new ParseRESTCommand(this); + } + } + + // Headers + private final String sessionToken; + private String installationId; + public String masterKey; + + /* package */ String httpPath; + /* package */ final JSONObject jsonParameters; + + private String operationSetUUID; + private String localId; + + public ParseRESTCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken) { + this( + httpPath, + httpMethod, + parameters != null ? (JSONObject) NoObjectsEncoder.get().encode(parameters) : null, + sessionToken); + } + + public ParseRESTCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject jsonParameters, + String sessionToken) { + this(httpPath, httpMethod, jsonParameters, null, sessionToken); + } + + private ParseRESTCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject jsonParameters, + String localId, String sessionToken) { + super(httpMethod, createUrl(httpPath)); + + this.httpPath = httpPath; + this.jsonParameters = jsonParameters; + this.localId = localId; + this.sessionToken = sessionToken; + } + + /* package */ ParseRESTCommand(Init builder) { + super(builder.method, createUrl(builder.httpPath)); + sessionToken = builder.sessionToken; + installationId = builder.installationId; + masterKey = builder.masterKey; + + httpPath = builder.httpPath; + jsonParameters = builder.jsonParameters; + operationSetUUID = builder.operationSetUUID; + localId = builder.localId; + } + + public static ParseRESTCommand fromJSONObject(JSONObject jsonObject) { + String httpPath = jsonObject.optString("httpPath"); + ParseHttpRequest.Method httpMethod = + ParseHttpRequest.Method.fromString(jsonObject.optString("httpMethod")); + String sessionToken = jsonObject.optString("sessionToken", null); + String localId = jsonObject.optString("localId", null); + JSONObject jsonParameters = jsonObject.optJSONObject("parameters"); + + return new ParseRESTCommand(httpPath, httpMethod, jsonParameters, localId, sessionToken); + } + + private static String createUrl(String httpPath) { + // We send all parameters for GET/HEAD/DELETE requests in a post body, + // so no need to worry about query parameters here. + if (httpPath == null) { + return server.toString(); + } + + try { + return new URL(server, httpPath).toString(); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + } + + protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) { + if (installationId != null) { + requestBuilder.addHeader(HEADER_INSTALLATION_ID, installationId); + } + if (sessionToken != null) { + requestBuilder.addHeader(HEADER_SESSION_TOKEN, sessionToken); + } + if (masterKey != null) { + requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey); + } + } + + @Override + protected ParseHttpRequest newRequest( + ParseHttpRequest.Method method, + String url, + ProgressCallback uploadProgressCallback) { + ParseHttpRequest request; + if (jsonParameters != null && + method != ParseHttpRequest.Method.POST && + method != ParseHttpRequest.Method.PUT) { + // The request URI may be too long to include parameters in the URI. + // To avoid this problem we send the parameters in a POST request json-encoded body + // and add a http method override parameter in newBody. + request = super.newRequest(ParseHttpRequest.Method.POST, url, uploadProgressCallback); + } else { + request = super.newRequest(method, url, uploadProgressCallback); + } + ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder(request); + addAdditionalHeaders(requestBuilder); + return requestBuilder.build(); + } + + @Override + protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) { + if (jsonParameters == null) { + String message = String.format("Trying to execute a %s command without body parameters.", + method.toString()); + throw new IllegalArgumentException(message); + } + + try { + JSONObject parameters = jsonParameters; + if (method == ParseHttpRequest.Method.GET || + method == ParseHttpRequest.Method.DELETE) { + // The request URI may be too long to include parameters in the URI. + // To avoid this problem we send the parameters in a POST request json-encoded body + // and add a http method override parameter. + parameters = new JSONObject(jsonParameters.toString()); + parameters.put(PARAMETER_METHOD_OVERRIDE, method.toString()); + } + return new ParseByteArrayHttpBody(parameters.toString(), "application/json"); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + public Task executeAsync( + final ParseHttpClient client, + final ProgressCallback uploadProgressCallback, + final ProgressCallback downloadProgressCallback, + final Task cancellationToken) { + resolveLocalIds(); + return super.executeAsync( + client, uploadProgressCallback, downloadProgressCallback, cancellationToken); + } + + @Override + protected Task onResponseAsync(ParseHttpResponse response, + ProgressCallback downloadProgressCallback) { + String content; + InputStream responseStream = null; + try { + responseStream = response.getContent(); + content = new String(ParseIOUtils.toByteArray(responseStream)); + } catch (IOException e) { + return Task.forError(e); + } finally { + ParseIOUtils.closeQuietly(responseStream); + } + + // We need to check for errors differently in /1/ than /2/ since object data in /2/ was + // encapsulated in "data" and everything was 200, but /2/ everything is in the root JSON, + // but errors are status 4XX. + // See https://quip.com/4pbbA9HbOPjQ + int statusCode = response.getStatusCode(); + if (statusCode >= 200 && statusCode < 600) { // Assume 3XX is handled by http library + JSONObject json; + try { + json = new JSONObject(content); + + if (statusCode >= 400 && statusCode < 500) { // 4XX + return Task.forError(newPermanentException(json.optInt("code"), json.optString("error"))); + } else if (statusCode >= 500) { // 5XX + return Task.forError(newTemporaryException(json.optInt("code"), json.optString("error"))); + } + + return Task.forResult(json); + } catch (JSONException e) { + return Task.forError(newTemporaryException("bad json response", e)); + } + } + + return Task.forError(newPermanentException(ParseException.OTHER_CAUSE, content)); + } + + // Creates a somewhat-readable string that uniquely identifies this command. + public String getCacheKey() { + String json; + if (jsonParameters != null) { + try { + json = toDeterministicString(jsonParameters); + } catch (JSONException e) { + throw new RuntimeException(e.getMessage()); + } + } else { + json = ""; + } + + // Include the session token in the cache in order to avoid mixing permissions. + if (sessionToken != null) { + json += sessionToken; + } + + return String.format( + "ParseRESTCommand.%s.%s.%s", + method.toString(), + ParseDigestUtils.md5(httpPath), + ParseDigestUtils.md5(json) + ); + } + + // Encodes the object to JSON, but ensures that JSONObjects + // and nested JSONObjects are encoded with keys in alphabetical order. + /** package */ static String toDeterministicString(Object o) throws JSONException { + JSONStringer stringer = new JSONStringer(); + addToStringer(stringer, o); + return stringer.toString(); + } + + // Uses the provided JSONStringer to encode this object to JSON, but ensures that JSONObjects and + // nested JSONObjects are encoded with keys in alphabetical order. + private static void addToStringer(JSONStringer stringer, Object o) throws JSONException { + if (o instanceof JSONObject) { + stringer.object(); + JSONObject object = (JSONObject) o; + Iterator keyIterator = object.keys(); + ArrayList keys = new ArrayList<>(); + while (keyIterator.hasNext()) { + keys.add(keyIterator.next()); + } + Collections.sort(keys); + + for (String key : keys) { + stringer.key(key); + addToStringer(stringer, object.opt(key)); + } + + stringer.endObject(); + return; + } + + if (o instanceof JSONArray) { + JSONArray array = (JSONArray) o; + stringer.array(); + for (int i = 0; i < array.length(); ++i) { + addToStringer(stringer, array.get(i)); + } + stringer.endArray(); + return; + } + + stringer.value(o); + } + + /* package */ static boolean isValidCommandJSONObject(JSONObject jsonObject) { + return jsonObject.has("httpPath"); + } + + // This function checks whether a json object is a valid /2 ParseCommand json. + /* package */ static boolean isValidOldFormatCommandJSONObject(JSONObject jsonObject) { + return jsonObject.has("op"); + } + + public JSONObject toJSONObject() { + JSONObject jsonObject = new JSONObject(); + try { + if (httpPath != null) { + jsonObject.put("httpPath", httpPath); + } + jsonObject.put("httpMethod", method.toString()); + if (jsonParameters != null) { + jsonObject.put("parameters", jsonParameters); + } + if (sessionToken != null) { + jsonObject.put("sessionToken", sessionToken); + } + if (localId != null) { + jsonObject.put("localId", localId); + } + } catch (JSONException e) { + throw new RuntimeException(e.getMessage()); + } + return jsonObject; + } + + public String getSessionToken() { + return sessionToken; + } + + public String getOperationSetUUID() { + return operationSetUUID; + } + + /* package */ void setOperationSetUUID(String operationSetUUID) { + this.operationSetUUID = operationSetUUID; + } + + public void setLocalId(String localId) { + this.localId = localId; + } + + public String getLocalId() { + return localId; + } + + + /** + * If this was the second save on a new object while offline, then its objectId wasn't yet set + * when the command was created, so it would have been considered a "create". But if the first + * save succeeded, then there is an objectId now, and it will be mapped to the localId for this + * command's result. If so, change the "create" operation to an "update", and add the objectId to + * the command. + */ + private void maybeChangeServerOperation() throws JSONException { + if (localId != null) { + String objectId = getLocalIdManager().getObjectId(localId); + if (objectId != null) { + localId = null; + httpPath += String.format("/%s", objectId); + url = createUrl(httpPath); + + if (httpPath.startsWith("classes") && method == ParseHttpRequest.Method.POST) { + method = ParseHttpRequest.Method.PUT; + } + } + } + } + + public void resolveLocalIds() { + try { + ArrayList localPointers = new ArrayList<>(); + getLocalPointersIn(jsonParameters, localPointers); + for (JSONObject pointer : localPointers) { + String localId = (String) pointer.get("localId"); + String objectId = getLocalIdManager().getObjectId(localId); + if (objectId == null) { + throw new IllegalStateException( + "Tried to serialize a command referencing a new, unsaved object."); + } + pointer.put("objectId", objectId); + pointer.remove("localId"); + } + maybeChangeServerOperation(); + } catch (JSONException e) { + // Well, nothing to do here... + } + } + + /** + * Finds all of the local ids in this command and increments their retain counts in the on-disk + * store. This should be called immediately before serializing the command to disk, so that we + * know we might need to resolve these local ids at some point in the future. + */ + public void retainLocalIds() { + if (localId != null) { + getLocalIdManager().retainLocalIdOnDisk(localId); + } + + try { + ArrayList localPointers = new ArrayList<>(); + getLocalPointersIn(jsonParameters, localPointers); + for (JSONObject pointer : localPointers) { + String localId = (String) pointer.get("localId"); + getLocalIdManager().retainLocalIdOnDisk(localId); + } + } catch (JSONException e) { + // Well, nothing to do here... + } + } + + /** + * Finds all of the local ids in this command and decrements their retain counts in the on-disk + * store. This should be called when removing a serialized command from the disk, when we know + * that we will never need to resolve these local ids for this command again in the future. + */ + public void releaseLocalIds() { + if (localId != null) { + getLocalIdManager().releaseLocalIdOnDisk(localId); + } + try { + ArrayList localPointers = new ArrayList<>(); + getLocalPointersIn(jsonParameters, localPointers); + for (JSONObject pointer : localPointers) { + String localId = (String) pointer.get("localId"); + getLocalIdManager().releaseLocalIdOnDisk(localId); + } + } catch (JSONException e) { + // Well, nothing to do here... + } + } + + protected static void getLocalPointersIn(Object container, ArrayList localPointers) + throws JSONException { + if (container instanceof JSONObject) { + JSONObject object = (JSONObject) container; + if ("Pointer".equals(object.opt("__type")) && object.has("localId")) { + localPointers.add((JSONObject) container); + return; + } + + Iterator keyIterator = object.keys(); + while (keyIterator.hasNext()) { + String key = keyIterator.next(); + getLocalPointersIn(object.get(key), localPointers); + } + } + + if (container instanceof JSONArray) { + JSONArray array = (JSONArray) container; + for (int i = 0; i < array.length(); ++i) { + getLocalPointersIn(array.get(i), localPointers); + } + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTConfigCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTConfigCommand.java new file mode 100644 index 0000000..2b280e8 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTConfigCommand.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import java.util.HashMap; +import java.util.Map; + +/** package */ class ParseRESTConfigCommand extends ParseRESTCommand { + + public ParseRESTConfigCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } + + public static ParseRESTConfigCommand fetchConfigCommand(String sessionToken) { + return new ParseRESTConfigCommand("config", ParseHttpRequest.Method.GET, null, sessionToken); + } + + public static ParseRESTConfigCommand updateConfigCommand( + final Map configParameters, String sessionToken) { + Map> commandParameters = null; + if (configParameters != null) { + commandParameters = new HashMap<>(); + commandParameters.put("params", configParameters); + } + return new ParseRESTConfigCommand( + "config", ParseHttpRequest.Method.PUT, commandParameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTFileCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTFileCommand.java new file mode 100644 index 0000000..9ced369 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTFileCommand.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * REST network command for creating & uploading {@link ParseFile}s. + */ + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; + +import java.io.File; + +/** package */ class ParseRESTFileCommand extends ParseRESTCommand { + + public static class Builder extends Init { + + private byte[] data = null; + private String contentType = null; + private File file; + + public Builder() { + // We only ever use ParseRESTFileCommand for file uploads, so default to POST. + method(ParseHttpRequest.Method.POST); + } + + public Builder fileName(String fileName) { + return httpPath(String.format("files/%s", fileName)); + } + + public Builder data(byte[] data) { + this.data = data; + return this; + } + + public Builder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder file(File file) { + this.file = file; + return this; + } + + @Override + /* package */ Builder self() { + return this; + } + + public ParseRESTFileCommand build() { + return new ParseRESTFileCommand(this); + } + } + + private final byte[] data; + private final String contentType; + private final File file; + + public ParseRESTFileCommand(Builder builder) { + super(builder); + if (builder.file != null && builder.data != null) { + throw new IllegalArgumentException("File and data can not be set at the same time"); + } + this.data = builder.data; + this.contentType = builder.contentType; + this.file = builder.file; + } + + @Override + protected ParseHttpBody newBody(final ProgressCallback progressCallback) { + // TODO(mengyan): Delete ParseByteArrayHttpBody when we change input byte array to staged file + // in ParseFileController + if (progressCallback == null) { + return data != null ? + new ParseByteArrayHttpBody(data, contentType) : new ParseFileHttpBody(file, contentType); + } + return data != null ? + new ParseCountingByteArrayHttpBody(data, contentType, progressCallback) : + new ParseCountingFileHttpBody(file, contentType, progressCallback); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java new file mode 100644 index 0000000..4fcaafa --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** package */ class ParseRESTObjectBatchCommand extends ParseRESTCommand { + public final static int COMMAND_OBJECT_BATCH_MAX_SIZE = 50; + + private static final String KEY_RESULTS = "results"; + + public static List> executeBatch( + ParseHttpClient client, List commands, String sessionToken) { + final int batchSize = commands.size(); + List> tasks = new ArrayList<>(batchSize); + + if (batchSize == 1) { + // There's only one, just execute it + tasks.add(commands.get(0).executeAsync(client)); + return tasks; + } + + if (batchSize > COMMAND_OBJECT_BATCH_MAX_SIZE) { + // There's more than the max, split it up into batches + List> batches = Lists.partition(commands, + COMMAND_OBJECT_BATCH_MAX_SIZE); + for (int i = 0, size = batches.size(); i < size; i++) { + List batch = batches.get(i); + tasks.addAll(executeBatch(client, batch, sessionToken)); + } + return tasks; + } + + final List> tcss = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + tcss.add(tcs); + tasks.add(tcs.getTask()); + } + + JSONObject parameters = new JSONObject(); + JSONArray requests = new JSONArray(); + try { + for (ParseRESTObjectCommand command : commands) { + JSONObject requestParameters = new JSONObject(); + requestParameters.put("method", command.method.toString()); + requestParameters.put("path", new URL(server, command.httpPath).getPath()); + JSONObject body = command.jsonParameters; + if (body != null) { + requestParameters.put("body", body); + } + requests.put(requestParameters); + } + parameters.put("requests", requests); + } catch (JSONException | MalformedURLException e) { + throw new RuntimeException(e); + } + + ParseRESTCommand command = new ParseRESTObjectBatchCommand( + "batch", ParseHttpRequest.Method.POST, parameters, sessionToken); + + command.executeAsync(client).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + TaskCompletionSource tcs; + + if (task.isFaulted() || task.isCancelled()) { + // REST command failed or canceled, fail or cancel all tasks + for (int i = 0; i < batchSize; i++) { + tcs = tcss.get(i); + if (task.isFaulted()) { + tcs.setError(task.getError()); + } else { + tcs.setCancelled(); + } + } + } + + JSONObject json = task.getResult(); + JSONArray results = json.getJSONArray(KEY_RESULTS); + + int resultLength = results.length(); + if (resultLength != batchSize) { + // Invalid response, fail all tasks + for (int i = 0; i < batchSize; i++) { + tcs = tcss.get(i); + tcs.setError(new IllegalStateException( + "Batch command result count expected: " + batchSize + " but was: " + resultLength)); + } + } + + for (int i = 0; i < batchSize; i++) { + JSONObject result = results.getJSONObject(i); + tcs = tcss.get(i); + + if (result.has("success")) { + JSONObject success = result.getJSONObject("success"); + tcs.setResult(success); + } else if (result.has("error")) { + JSONObject error = result.getJSONObject("error"); + tcs.setError(new ParseException(error.getInt("code"), error.getString("error"))); + } + } + return null; + } + }); + + return tasks; + } + + private ParseRESTObjectBatchCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } + + /** + * /batch is the only endpoint that doesn't return a JSONObject... It returns a JSONArray, but + * let's wrap that with a JSONObject {@code { "results": <original response%gt; }}. + */ + @Override + protected Task onResponseAsync(ParseHttpResponse response, + ProgressCallback downloadProgressCallback) { + InputStream responseStream = null; + String content = null; + try { + responseStream = response.getContent(); + content = new String(ParseIOUtils.toByteArray(responseStream)); + } catch (IOException e) { + return Task.forError(e); + } finally { + ParseIOUtils.closeQuietly(responseStream); + } + + JSONObject json; + try { + JSONArray results = new JSONArray(content); + json = new JSONObject(); + json.put(KEY_RESULTS, results); + } catch (JSONException e) { + return Task.forError(newTemporaryException("bad json response", e)); + } + + return Task.forResult(json); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectCommand.java new file mode 100644 index 0000000..31fbd61 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTObjectCommand.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.net.Uri; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONObject; + +/** package */ class ParseRESTObjectCommand extends ParseRESTCommand { + + public ParseRESTObjectCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } + + public static ParseRESTObjectCommand getObjectCommand(String objectId, String className, + String sessionToken) { + String httpPath = String.format("classes/%s/%s", Uri.encode(className), Uri.encode(objectId)); + return new ParseRESTObjectCommand(httpPath, ParseHttpRequest.Method.GET, null, sessionToken); + } + + public static ParseRESTObjectCommand saveObjectCommand( + ParseObject.State state, JSONObject operations, String sessionToken) { + if (state.objectId() == null) { + return ParseRESTObjectCommand.createObjectCommand( + state.className(), + operations, + sessionToken); + } else { + return ParseRESTObjectCommand.updateObjectCommand( + state.objectId(), + state.className(), + operations, + sessionToken); + } + } + + private static ParseRESTObjectCommand createObjectCommand(String className, JSONObject changes, + String sessionToken) { + String httpPath = String.format("classes/%s", Uri.encode(className)); + return new ParseRESTObjectCommand(httpPath, ParseHttpRequest.Method.POST, changes, sessionToken); + } + + private static ParseRESTObjectCommand updateObjectCommand(String objectId, String className, + JSONObject changes, String sessionToken) { + String httpPath = String.format("classes/%s/%s", Uri.encode(className), Uri.encode(objectId)); + return new ParseRESTObjectCommand(httpPath, ParseHttpRequest.Method.PUT, changes, sessionToken); + } + + public static ParseRESTObjectCommand deleteObjectCommand( + ParseObject.State state, String sessionToken) { + String httpPath = String.format("classes/%s", Uri.encode(state.className())); + String objectId = state.objectId(); + if (objectId != null) { + httpPath += String.format("/%s", Uri.encode(objectId)); + } + return new ParseRESTObjectCommand(httpPath, ParseHttpRequest.Method.DELETE, null, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTPushCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTPushCommand.java new file mode 100644 index 0000000..83ec327 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTPushCommand.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Set; + +/** package */ class ParseRESTPushCommand extends ParseRESTCommand { + + /* package */ final static String KEY_CHANNELS = "channels"; + /* package */ final static String KEY_WHERE = "where"; + /* package */ final static String KEY_DEVICE_TYPE = "deviceType"; + /* package */ final static String KEY_EXPIRATION_TIME = "expiration_time"; + /* package */ final static String KEY_EXPIRATION_INTERVAL = "expiration_interval"; + /* package */ final static String KEY_PUSH_TIME = "push_time"; + /* package */ final static String KEY_DATA = "data"; + + public ParseRESTPushCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } + + public static ParseRESTPushCommand sendPushCommand(ParseQuery.State query, + Set targetChannels, String targetDeviceType, Long expirationTime, + Long expirationInterval, Long pushTime, JSONObject payload, String sessionToken) { + JSONObject parameters = new JSONObject(); + try { + if (targetChannels != null) { + parameters.put(KEY_CHANNELS, new JSONArray(targetChannels)); + } else { + JSONObject whereJSON = null; + if (query != null) { + ParseQuery.QueryConstraints where = query.constraints(); + whereJSON = (JSONObject) PointerEncoder.get().encode(where); + } + if (targetDeviceType != null) { + whereJSON = new JSONObject(); + whereJSON.put(KEY_DEVICE_TYPE, targetDeviceType); + } + if (whereJSON == null) { + // If there are no conditions set, then push to everyone by specifying empty query conditions. + whereJSON = new JSONObject(); + } + parameters.put(KEY_WHERE, whereJSON); + } + + if (expirationTime != null) { + parameters.put(KEY_EXPIRATION_TIME, expirationTime); + } else if (expirationInterval != null) { + parameters.put(KEY_EXPIRATION_INTERVAL, expirationInterval); + } + + if (pushTime != null) { + parameters.put(KEY_PUSH_TIME, pushTime); + } + + if (payload != null) { + parameters.put(KEY_DATA, payload); + } + + } catch (JSONException e) { + throw new RuntimeException(e); + } + return new ParseRESTPushCommand("push", ParseHttpRequest.Method.POST, parameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTQueryCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTQueryCommand.java new file mode 100644 index 0000000..a63df6e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTQueryCommand.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** package */ class ParseRESTQueryCommand extends ParseRESTCommand { + + /* package */ final static String KEY_ORDER = "order"; + /* package */ final static String KEY_WHERE = "where"; + /* package */ final static String KEY_KEYS = "keys"; + /* package */ final static String KEY_INCLUDE = "include"; + /* package */ final static String KEY_LIMIT = "limit"; + /* package */ final static String KEY_COUNT = "count"; + /* package */ final static String KEY_SKIP = "skip"; + /* package */ final static String KEY_TRACE = "trace"; + + public static ParseRESTQueryCommand findCommand( + ParseQuery.State state, String sessionToken) { + String httpPath = String.format("classes/%s", state.className()); + Map parameters = encode(state, false); + return new ParseRESTQueryCommand( + httpPath, ParseHttpRequest.Method.GET, parameters, sessionToken); + } + + public static ParseRESTQueryCommand countCommand( + ParseQuery.State state, String sessionToken) { + String httpPath = String.format("classes/%s", state.className()); + Map parameters = encode(state, true); + return new ParseRESTQueryCommand( + httpPath, ParseHttpRequest.Method.GET, parameters, sessionToken); + } + + /* package */ static Map encode( + ParseQuery.State state, boolean count) { + ParseEncoder encoder = PointerEncoder.get(); + HashMap parameters = new HashMap<>(); + List order = state.order(); + if (!order.isEmpty()) { + parameters.put(KEY_ORDER, ParseTextUtils.join(",", order)); + } + + ParseQuery.QueryConstraints conditions = state.constraints(); + if (!conditions.isEmpty()) { + JSONObject encodedConditions = + (JSONObject) encoder.encode(conditions); + parameters.put(KEY_WHERE, encodedConditions.toString()); + } + + // This is nullable since we allow unset selectedKeys as well as no selectedKeys + Set selectedKeys = state.selectedKeys(); + if (selectedKeys != null) { + parameters.put(KEY_KEYS, ParseTextUtils.join(",", selectedKeys)); + } + + Set includeds = state.includes(); + if (!includeds.isEmpty()) { + parameters.put(KEY_INCLUDE, ParseTextUtils.join(",", includeds)); + } + + // Respect what the caller wanted for limit, even if count is true, because + // parse-server supports it. Currently with our APIs, when counting, limit will always be 0, + // but that logic is in ParseQuery class and we should not do that again here. + int limit = state.limit(); + if (limit >= 0) { + parameters.put(KEY_LIMIT, Integer.toString(limit)); + } + + if (count) { + parameters.put(KEY_COUNT, Integer.toString(1)); + } else { + int skip = state.skip(); + if (skip > 0) { + parameters.put(KEY_SKIP, Integer.toString(skip)); + } + } + + Map extraOptions = state.extraOptions(); + for (Map.Entry entry : extraOptions.entrySet()) { + Object encodedExtraOptions = encoder.encode(entry.getValue()); + parameters.put(entry.getKey(), encodedExtraOptions.toString()); + } + + if (state.isTracingEnabled()) { + parameters.put(KEY_TRACE, Integer.toString(1)); + } + return parameters; + } + + private ParseRESTQueryCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken) { + super(httpPath, httpMethod, parameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTSessionCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTSessionCommand.java new file mode 100644 index 0000000..1472528 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTSessionCommand.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONObject; + +/** package */ class ParseRESTSessionCommand extends ParseRESTCommand { + + public static ParseRESTSessionCommand getCurrentSessionCommand(String sessionToken) { + return new ParseRESTSessionCommand( + "sessions/me", ParseHttpRequest.Method.GET, null, sessionToken); + } + + public static ParseRESTSessionCommand revoke(String sessionToken) { + return new ParseRESTSessionCommand( + "logout", ParseHttpRequest.Method.POST, new JSONObject(), sessionToken); + } + + public static ParseRESTSessionCommand upgradeToRevocableSessionCommand(String sessionToken) { + return new ParseRESTSessionCommand( + "upgradeToRevocableSession", ParseHttpRequest.Method.POST, new JSONObject(), sessionToken); + } + + private ParseRESTSessionCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject jsonParameters, + String sessionToken) { + super(httpPath, httpMethod, jsonParameters, sessionToken); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTUserCommand.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTUserCommand.java new file mode 100644 index 0000000..f4877cc --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRESTUserCommand.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +import bolts.Task; + +/** package */ class ParseRESTUserCommand extends ParseRESTCommand { + + private static final String HEADER_REVOCABLE_SESSION = "X-Parse-Revocable-Session"; + private static final String HEADER_TRUE = "1"; + + public static ParseRESTUserCommand getCurrentUserCommand(String sessionToken) { + return new ParseRESTUserCommand("users/me", ParseHttpRequest.Method.GET, null, sessionToken); + } + + //region Authentication + + public static ParseRESTUserCommand signUpUserCommand(JSONObject parameters, String sessionToken, + boolean revocableSession) { + return new ParseRESTUserCommand( + "users", ParseHttpRequest.Method.POST, parameters, sessionToken, revocableSession); + } + + public static ParseRESTUserCommand logInUserCommand(String username, String password, + boolean revocableSession) { + Map parameters = new HashMap<>(); + parameters.put("username", username); + parameters.put("password", password); + return new ParseRESTUserCommand( + "login", ParseHttpRequest.Method.GET, parameters, null, revocableSession); + } + + public static ParseRESTUserCommand serviceLogInUserCommand( + String authType, Map authData, boolean revocableSession) { + + // Mimic ParseSetOperation + JSONObject parameters; + try { + JSONObject authenticationData = new JSONObject(); + authenticationData.put(authType, PointerEncoder.get().encode(authData)); + + parameters = new JSONObject(); + parameters.put("authData", authenticationData); + } catch (JSONException e) { + throw new RuntimeException("could not serialize object to JSON"); + } + + return serviceLogInUserCommand(parameters, null, revocableSession); + } + + public static ParseRESTUserCommand serviceLogInUserCommand(JSONObject parameters, + String sessionToken, boolean revocableSession) { + return new ParseRESTUserCommand( + "users", ParseHttpRequest.Method.POST, parameters, sessionToken, revocableSession); + } + + //endregion + + public static ParseRESTUserCommand resetPasswordResetCommand(String email) { + Map parameters = new HashMap<>(); + parameters.put("email", email); + return new ParseRESTUserCommand( + "requestPasswordReset", ParseHttpRequest.Method.POST, parameters, null); + } + + private boolean isRevocableSessionEnabled; + private int statusCode; + + private ParseRESTUserCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken) { + this(httpPath, httpMethod, parameters, sessionToken, false); + } + + private ParseRESTUserCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + Map parameters, + String sessionToken, boolean isRevocableSessionEnabled) { + super(httpPath, httpMethod, parameters, sessionToken); + this.isRevocableSessionEnabled = isRevocableSessionEnabled; + } + + private ParseRESTUserCommand( + String httpPath, + ParseHttpRequest.Method httpMethod, + JSONObject parameters, + String sessionToken, boolean isRevocableSessionEnabled) { + super(httpPath, httpMethod, parameters, sessionToken); + this.isRevocableSessionEnabled = isRevocableSessionEnabled; + } + + public int getStatusCode() { + return statusCode; + } + + @Override + protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) { + super.addAdditionalHeaders(requestBuilder); + if (isRevocableSessionEnabled) { + requestBuilder.addHeader(HEADER_REVOCABLE_SESSION, HEADER_TRUE); + } + } + + @Override + protected Task onResponseAsync(ParseHttpResponse response, + ProgressCallback progressCallback) { + statusCode = response.getStatusCode(); + return super.onResponseAsync(response, progressCallback); + } + +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelation.java new file mode 100644 index 0000000..4a6a172 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelation.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A class that is used to access all of the children of a many-to-many relationship. Each instance + * of Parse.Relation is associated with a particular parent object and key. + */ +public class ParseRelation implements Parcelable { + private final Object mutex = new Object(); + + // The owning object of this ParseRelation. + private WeakReference parent; + + // The object Id of the parent. + private String parentObjectId; + + // The classname of the parent to retrieve the parent ParseObject in case the parent is GC'ed. + private String parentClassName; + + // The key of the relation in the parent object. + private String key; + + // The className of the target objects. + private String targetClass; + + // For offline caching, we keep track of every object we've known to be in the relation. + private Set knownObjects = new HashSet<>(); + + /* package */ ParseRelation(ParseObject parent, String key) { + this.parent = new WeakReference<>(parent); + this.parentObjectId = parent.getObjectId(); + this.parentClassName = parent.getClassName(); + this.key = key; + this.targetClass = null; + } + + /* package */ ParseRelation(String targetClass) { + this.parent = null; + this.parentObjectId = null; + this.parentClassName = null; + this.key = null; + this.targetClass = targetClass; + } + + /** + * Parses a relation from JSON with the given decoder. + */ + /* package */ ParseRelation(JSONObject jsonObject, ParseDecoder decoder) { + this.parent = null; + this.parentObjectId = null; + this.parentClassName = null; + this.key = null; + this.targetClass = jsonObject.optString("className", null); + JSONArray objectsArray = jsonObject.optJSONArray("objects"); + if (objectsArray != null) { + for (int i = 0; i < objectsArray.length(); ++i) { + knownObjects.add((ParseObject)decoder.decode(objectsArray.optJSONObject(i))); + } + } + } + + /** + * Creates a ParseRelation from a Parcel with the given decoder. + */ + /* package */ ParseRelation(Parcel source, ParseParcelDecoder decoder) { + if (source.readByte() == 1) this.key = source.readString(); + if (source.readByte() == 1) this.targetClass = source.readString(); + if (source.readByte() == 1) this.parentClassName = source.readString(); + if (source.readByte() == 1) this.parentObjectId = source.readString(); + if (source.readByte() == 1) this.parent = new WeakReference<>((ParseObject) decoder.decode(source)); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + knownObjects.add((ParseObject) decoder.decode(source)); + } + } + + /* package */ void ensureParentAndKey(ParseObject someParent, String someKey) { + synchronized (mutex) { + if (parent == null) { + parent = new WeakReference<>(someParent); + parentObjectId = someParent.getObjectId(); + parentClassName = someParent.getClassName(); + } + if (key == null) { + key = someKey; + } + if (parent.get() != someParent) { + throw new IllegalStateException( + "Internal error. One ParseRelation retrieved from two different ParseObjects."); + } + if (!key.equals(someKey)) { + throw new IllegalStateException( + "Internal error. One ParseRelation retrieved from two different keys."); + } + } + } + + /** + * Adds an object to this relation. + * + * @param object + * The object to add to this relation. + */ + public void add(T object) { + synchronized (mutex) { + ParseRelationOperation operation = + new ParseRelationOperation<>(Collections.singleton(object), null); + targetClass = operation.getTargetClass(); + getParent().performOperation(key, operation); + + knownObjects.add(object); + } + } + + /** + * Removes an object from this relation. + * + * @param object + * The object to remove from this relation. + */ + public void remove(T object) { + synchronized (mutex) { + ParseRelationOperation operation = + new ParseRelationOperation<>(null, Collections.singleton(object)); + targetClass = operation.getTargetClass(); + getParent().performOperation(key, operation); + + knownObjects.remove(object); + } + } + + /** + * Gets a query that can be used to query the objects in this relation. + * + * @return A ParseQuery that restricts the results to objects in this relations. + */ + public ParseQuery getQuery() { + synchronized (mutex) { + ParseQuery.State.Builder builder; + if (targetClass == null) { + builder = new ParseQuery.State.Builder(parentClassName) + .redirectClassNameForKey(key); + } else { + builder = new ParseQuery.State.Builder<>(targetClass); + } + builder.whereRelatedTo(getParent(), key); + return new ParseQuery<>(builder); + } + } + + /* package */ JSONObject encodeToJSON(ParseEncoder objectEncoder) throws JSONException { + synchronized (mutex) { + JSONObject relation = new JSONObject(); + relation.put("__type", "Relation"); + relation.put("className", targetClass); + JSONArray knownObjectsArray = new JSONArray(); + for (ParseObject knownObject : knownObjects) { + try { + knownObjectsArray.put(objectEncoder.encodeRelatedObject(knownObject)); + } catch (Exception e) { + // This is just for caching, so if an object can't be encoded for any reason, drop it. + } + } + relation.put("objects", knownObjectsArray); + return relation; + } + } + + /* package */ String getTargetClass() { + synchronized (mutex) { + return targetClass; + } + } + + /* package */ void setTargetClass(String className) { + synchronized (mutex) { + targetClass = className; + } + } + + /** + * Adds an object that is known to be in the relation. This is used for offline caching. + */ + /* package */ void addKnownObject(ParseObject object) { + synchronized (mutex) { + knownObjects.add(object); + } + } + + /** + * Removes an object that is known to not be in the relation. This is used for offline caching. + */ + /* package */ void removeKnownObject(ParseObject object) { + synchronized (mutex) { + knownObjects.remove(object); + } + } + + /** + * Returns true iff this object was ever known to be in the relation. This is used for offline + * caching. + */ + /* package */ boolean hasKnownObject(ParseObject object) { + synchronized (mutex) { + return knownObjects.contains(object); + } + } + + /* package for tests */ ParseObject getParent() { + if(parent == null){ + return null; + } + if(parent.get() == null){ + return ParseObject.createWithoutData(parentClassName, parentObjectId); + } + return parent.get(); + } + + /* package for tests */ String getKey() { + return key; + } + + /* package for tests */ Set getKnownObjects() { + return knownObjects; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, new ParseObjectParcelEncoder()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + synchronized (mutex) { + // Fields are all nullable. + dest.writeByte(key != null ? (byte) 1 : 0); + if (key != null) dest.writeString(key); + dest.writeByte(targetClass != null ? (byte) 1 : 0); + if (targetClass != null) dest.writeString(targetClass); + dest.writeByte(parentClassName != null ? (byte) 1 : 0); + if (parentClassName != null) dest.writeString(parentClassName); + dest.writeByte(parentObjectId != null ? (byte) 1 : 0); + if (parentObjectId != null) dest.writeString(parentObjectId); + boolean has = parent != null && parent.get() != null; + dest.writeByte(has ? (byte) 1 : 0); + if (has) encoder.encode(parent.get(), dest); + dest.writeInt(knownObjects.size()); + for (ParseObject obj : knownObjects) { + encoder.encode(obj, dest); + } + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseRelation createFromParcel(Parcel source) { + return new ParseRelation(source, new ParseObjectParcelDecoder()); + } + + @Override + public ParseRelation[] newArray(int size) { + return new ParseRelation[size]; + } + }; +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelationOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelationOperation.java new file mode 100644 index 0000000..a3075f3 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRelationOperation.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An operation where a ParseRelation's value is modified. + */ +/** package */ class ParseRelationOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME_ADD = "AddRelation"; + /* package */ final static String OP_NAME_REMOVE = "RemoveRelation"; + /* package */ final static String OP_NAME_BATCH = "Batch"; + + // The className of the target objects. + private final String targetClass; + + // A set of objects to add to this relation. + private final Set relationsToAdd; + // A set of objects to remove from this relation. + private final Set relationsToRemove; + + ParseRelationOperation(Set newRelationsToAdd, Set newRelationsToRemove) { + String targetClass = null; + relationsToAdd = new HashSet<>(); + relationsToRemove = new HashSet<>(); + + if (newRelationsToAdd != null) { + for (T object : newRelationsToAdd) { + addParseObjectToSet(object, relationsToAdd); + + if (targetClass == null) { + targetClass = object.getClassName(); + } else { + if (!targetClass.equals(object.getClassName())) { + throw new IllegalArgumentException( + "All objects in a relation must be of the same class."); + } + } + } + } + + if (newRelationsToRemove != null) { + for (T object : newRelationsToRemove) { + addParseObjectToSet(object, relationsToRemove); + + if (targetClass == null) { + targetClass = object.getClassName(); + } else { + if (!targetClass.equals(object.getClassName())) { + throw new IllegalArgumentException( + "All objects in a relation must be of the same class."); + } + } + } + } + + if (targetClass == null) { + throw new IllegalArgumentException("Cannot create a ParseRelationOperation with no objects."); + } + this.targetClass = targetClass; + } + + private ParseRelationOperation(String newTargetClass, Set newRelationsToAdd, + Set newRelationsToRemove) { + targetClass = newTargetClass; + relationsToAdd = new HashSet<>(newRelationsToAdd); + relationsToRemove = new HashSet<>(newRelationsToRemove); + } + + /* + * Adds a ParseObject to a set, replacing any existing instance of the same object. + */ + private void addParseObjectToSet(ParseObject obj, Set set) { + if (Parse.getLocalDatastore() != null || obj.getObjectId() == null) { + // There's no way there could be duplicate instances. + set.add(obj); + return; + } + + // We have to do this the hard way. + for (ParseObject existingObject : set) { + if (obj.getObjectId().equals(existingObject.getObjectId())) { + set.remove(existingObject); + } + } + set.add(obj); + } + + /* + * Adds a list of ParseObject to a set, replacing any existing instance of the same object. + */ + private void addAllParseObjectsToSet(Collection list, Set set) { + for (ParseObject obj : list) { + addParseObjectToSet(obj, set); + } + } + + /* + * Removes an object (and any duplicate instances of that object) from the set. + */ + private void removeParseObjectFromSet(ParseObject obj, Set set) { + if (Parse.getLocalDatastore() != null || obj.getObjectId() == null) { + // There's no way there could be duplicate instances. + set.remove(obj); + return; + } + + // We have to do this the hard way. + for (ParseObject existingObject : set) { + if (obj.getObjectId().equals(existingObject.getObjectId())) { + set.remove(existingObject); + } + } + } + + /* + * Removes all objects (and any duplicate instances of those objects) from the set. + */ + private void removeAllParseObjectsFromSet(Collection list, Set set) { + for (ParseObject obj : list) { + removeParseObjectFromSet(obj, set); + } + } + + String getTargetClass() { + return targetClass; + } + + /* + * Converts a set of objects into a JSONArray of Parse pointers. + */ + JSONArray convertSetToArray(Set set, ParseEncoder objectEncoder) + throws JSONException { + JSONArray array = new JSONArray(); + for (ParseObject obj : set) { + array.put(objectEncoder.encode(obj)); + } + return array; + } + + // Encodes any add/removes ops to JSON to send to the server. + @Override + public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { + JSONObject adds = null; + JSONObject removes = null; + + if (relationsToAdd.size() > 0) { + adds = new JSONObject(); + adds.put("__op", OP_NAME_ADD); + adds.put("objects", convertSetToArray(relationsToAdd, objectEncoder)); + } + + if (relationsToRemove.size() > 0) { + removes = new JSONObject(); + removes.put("__op", OP_NAME_REMOVE); + removes.put("objects", convertSetToArray(relationsToRemove, objectEncoder)); + } + + if (adds != null && removes != null) { + JSONObject result = new JSONObject(); + result.put("__op", OP_NAME_BATCH); + JSONArray ops = new JSONArray(); + ops.put(adds); + ops.put(removes); + result.put("ops", ops); + return result; + } + + if (adds != null) { + return adds; + } + + if (removes != null) { + return removes; + } + + throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + if (relationsToAdd.isEmpty() && relationsToRemove.isEmpty()) { + throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); + } + if (relationsToAdd.size() > 0 && relationsToRemove.size() > 0) { + dest.writeString(OP_NAME_BATCH); + } + if (relationsToAdd.size() > 0) { + dest.writeString(OP_NAME_ADD); + dest.writeInt(relationsToAdd.size()); + for (ParseObject object : relationsToAdd) { + parcelableEncoder.encode(object, dest); + } + } + if (relationsToRemove.size() > 0) { + dest.writeString(OP_NAME_REMOVE); + dest.writeInt(relationsToRemove.size()); + for (ParseObject object : relationsToRemove) { + parcelableEncoder.encode(object, dest); + } + } + } + + @Override + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + if (previous == null) { + return this; + + } else if (previous instanceof ParseDeleteOperation) { + throw new IllegalArgumentException("You can't modify a relation after deleting it."); + + } else if (previous instanceof ParseRelationOperation) { + @SuppressWarnings("unchecked") + ParseRelationOperation previousOperation = (ParseRelationOperation) previous; + + if (previousOperation.targetClass != null + && !previousOperation.targetClass.equals(targetClass)) { + throw new IllegalArgumentException("Related object object must be of class " + + previousOperation.targetClass + ", but " + targetClass + " was passed in."); + } + + Set newRelationsToAdd = new HashSet<>(previousOperation.relationsToAdd); + Set newRelationsToRemove = new HashSet<>(previousOperation.relationsToRemove); + if (relationsToAdd != null) { + addAllParseObjectsToSet(relationsToAdd, newRelationsToAdd); + removeAllParseObjectsFromSet(relationsToAdd, newRelationsToRemove); + } + if (relationsToRemove != null) { + removeAllParseObjectsFromSet(relationsToRemove, newRelationsToAdd); + addAllParseObjectsToSet(relationsToRemove, newRelationsToRemove); + } + return new ParseRelationOperation(targetClass, newRelationsToAdd, newRelationsToRemove); + + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } + + @Override + @SuppressWarnings("unchecked") + public Object apply(Object oldValue, String key) { + ParseRelation relation; + + if (oldValue == null) { + relation = new ParseRelation<>(targetClass); + + } else if (oldValue instanceof ParseRelation) { + relation = (ParseRelation) oldValue; + if (targetClass != null && !targetClass.equals(relation.getTargetClass())) { + throw new IllegalArgumentException("Related object object must be of class " + + relation.getTargetClass() + ", but " + targetClass + " was passed in."); + } + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + + for (ParseObject relationToAdd : relationsToAdd) { + relation.addKnownObject(relationToAdd); + } + for (ParseObject relationToRemove : relationsToRemove) { + relation.removeKnownObject(relationToRemove); + } + return relation; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRemoveOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRemoveOperation.java new file mode 100644 index 0000000..0f24bd8 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRemoveOperation.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An operation that removes every instance of an element from an array field. + */ +/** package */ class ParseRemoveOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Remove"; + + protected final HashSet objects = new HashSet<>(); + + public ParseRemoveOperation(Collection coll) { + objects.addAll(coll); + } + + @Override + public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { + JSONObject output = new JSONObject(); + output.put("__op", OP_NAME); + output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); + return output; + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + if (previous == null) { + return this; + } else if (previous instanceof ParseDeleteOperation) { + return new ParseSetOperation(objects); + } else if (previous instanceof ParseSetOperation) { + Object value = ((ParseSetOperation) previous).getValue(); + if (value instanceof JSONArray || value instanceof List) { + return new ParseSetOperation(this.apply(value, null)); + } else { + throw new IllegalArgumentException("You can only add an item to a List or JSONArray."); + } + } else if (previous instanceof ParseRemoveOperation) { + HashSet result = new HashSet<>(((ParseRemoveOperation) previous).objects); + result.addAll(objects); + return new ParseRemoveOperation(result); + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } + + @Override + public Object apply(Object oldValue, String key) { + if (oldValue == null) { + return new ArrayList<>(); + } else if (oldValue instanceof JSONArray) { + ArrayList old = ParseFieldOperations.jsonArrayAsArrayList((JSONArray) oldValue); + @SuppressWarnings("unchecked") + ArrayList newValue = (ArrayList) this.apply(old, key); + return new JSONArray(newValue); + } else if (oldValue instanceof List) { + ArrayList result = new ArrayList<>((List) oldValue); + result.removeAll(objects); + + // Remove the removed objects from "objects" -- the items remaining + // should be ones that weren't removed by object equality. + ArrayList objectsToBeRemoved = new ArrayList<>(objects); + objectsToBeRemoved.removeAll(result); + + // Build up set of object IDs for any ParseObjects in the remaining objects-to-be-removed + HashSet objectIds = new HashSet<>(); + for (Object obj : objectsToBeRemoved) { + if (obj instanceof ParseObject) { + objectIds.add(((ParseObject) obj).getObjectId()); + } + } + + // And iterate over "result" to see if any other ParseObjects need to be removed + Iterator resultIterator = result.iterator(); + while (resultIterator.hasNext()) { + Object obj = resultIterator.next(); + if (obj instanceof ParseObject && objectIds.contains(((ParseObject) obj).getObjectId())) { + resultIterator.remove(); + } + } + return result; + } else { + throw new IllegalArgumentException("Operation is invalid after previous operation."); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRequest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRequest.java new file mode 100644 index 0000000..e50f7ce --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRequest.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.support.annotation.NonNull; + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * ParseRequest takes an arbitrary HttpUriRequest and retries it a number of times with + * exponential backoff. + */ +abstract class ParseRequest { + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(@NonNull Runnable r) { + return new Thread(r, "ParseRequest.NETWORK_EXECUTOR-thread-" + mCount.getAndIncrement()); + } + }; + + /** + * We want to use more threads than default in {@code bolts.Executors} since most of the time + * the threads will be asleep waiting for data. + */ + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int MAX_POOL_SIZE = CPU_COUNT * 2 * 2 + 1; + private static final long KEEP_ALIVE_TIME = 1L; + private static final int MAX_QUEUE_SIZE = 128; + + private static ThreadPoolExecutor newThreadPoolExecutor(int corePoolSize, int maxPoolSize, + long keepAliveTime, TimeUnit timeUnit, BlockingQueue workQueue, + ThreadFactory threadFactory) { + ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, + keepAliveTime, timeUnit, workQueue, threadFactory); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + protected static final ExecutorService NETWORK_EXECUTOR = newThreadPoolExecutor( + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, + new LinkedBlockingQueue(MAX_QUEUE_SIZE), sThreadFactory); + + protected static final int DEFAULT_MAX_RETRIES = 4; + /* package */ static final long DEFAULT_INITIAL_RETRY_DELAY = 1000L; + + private static long defaultInitialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY; + + public static void setDefaultInitialRetryDelay(long delay) { + defaultInitialRetryDelay = delay; + } + public static long getDefaultInitialRetryDelay() { + return defaultInitialRetryDelay; + } + + private static int maxRetries() { + //typically happens just within tests + if (ParsePlugins.get() == null) { + return DEFAULT_MAX_RETRIES; + } else { + return ParsePlugins.get().configuration().maxRetries; + } + } + + /* package */ ParseHttpRequest.Method method; + /* package */ String url; + + public ParseRequest(String url) { + this(ParseHttpRequest.Method.GET, url); + } + + public ParseRequest(ParseHttpRequest.Method method, String url) { + this.method = method; + this.url = url; + } + + protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) { + // do nothing + return null; + } + + protected ParseHttpRequest newRequest( + ParseHttpRequest.Method method, + String url, + ProgressCallback uploadProgressCallback) { + ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder() + .setMethod(method) + .setUrl(url); + + switch (method) { + case GET: + case DELETE: + break; + case POST: + case PUT: + requestBuilder.setBody(newBody(uploadProgressCallback)); + break; + default: + throw new IllegalStateException("Invalid method " + method); + } + return requestBuilder.build(); + } + + /* + * Runs one iteration of the request. + */ + private Task sendOneRequestAsync( + final ParseHttpClient client, + final ParseHttpRequest request, + final ProgressCallback downloadProgressCallback) { + return Task.forResult(null).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseHttpResponse response = client.execute(request); + return onResponseAsync(response, downloadProgressCallback); + } + }, NETWORK_EXECUTOR).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted()) { + Exception error = task.getError(); + if (error instanceof IOException) { + return Task.forError(newTemporaryException("i/o failure", error)); + } + } + return task; + // Jump off the network executor so this task continuations won't steal network threads + } + }, Task.BACKGROUND_EXECUTOR); + } + + protected abstract Task onResponseAsync(ParseHttpResponse response, + ProgressCallback downloadProgressCallback); + + /* + * Starts retrying the block. + */ + public Task executeAsync(final ParseHttpClient client) { + return executeAsync(client, (ProgressCallback) null, null, null); + } + + public Task executeAsync(final ParseHttpClient client, Task cancellationToken) { + return executeAsync(client, (ProgressCallback) null, null, cancellationToken); + } + + public Task executeAsync( + final ParseHttpClient client, + final ProgressCallback uploadProgressCallback, + final ProgressCallback downloadProgressCallback) { + return executeAsync(client, uploadProgressCallback, downloadProgressCallback, null); + } + + public Task executeAsync( + final ParseHttpClient client, + final ProgressCallback uploadProgressCallback, + final ProgressCallback downloadProgressCallback, + Task cancellationToken) { + ParseHttpRequest request = newRequest(method, url, uploadProgressCallback); + return executeAsync( + client, + request, + downloadProgressCallback, + cancellationToken); + } + + // Although we can not cancel a single request, but we allow cancel between retries so we need a + // cancellationToken here. + private Task executeAsync( + final ParseHttpClient client, + final ParseHttpRequest request, + final ProgressCallback downloadProgressCallback, + final Task cancellationToken) { + long delay = defaultInitialRetryDelay + (long) (defaultInitialRetryDelay * Math.random()); + + return executeAsync( + client, + request, + 0, + delay, + downloadProgressCallback, + cancellationToken); + } + + private Task executeAsync( + final ParseHttpClient client, + final ParseHttpRequest request, + final int attemptsMade, + final long delay, + final ProgressCallback downloadProgressCallback, + final Task cancellationToken) { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + return sendOneRequestAsync(client, request, downloadProgressCallback).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + Exception e = task.getError(); + if (task.isFaulted() && e instanceof ParseException) { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + if (e instanceof ParseRequestException && + ((ParseRequestException) e).isPermanentFailure) { + return task; + } + + if (attemptsMade < maxRetries()) { + PLog.i("com.parse.ParseRequest", "Request failed. Waiting " + delay + + " milliseconds before attempt #" + (attemptsMade + 1)); + + final TaskCompletionSource retryTask = new TaskCompletionSource<>(); + ParseExecutors.scheduled().schedule(new Runnable() { + @Override + public void run() { + executeAsync( + client, + request, + attemptsMade + 1, + delay * 2, + downloadProgressCallback, + cancellationToken).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isCancelled()) { + retryTask.setCancelled(); + } else if (task.isFaulted()) { + retryTask.setError(task.getError()); + } else { + retryTask.setResult(task.getResult()); + } + return null; + } + }); + } + }, delay, TimeUnit.MILLISECONDS); + return retryTask.getTask(); + } + } + return task; + } + }); + } + + /** + * Constructs a permanent exception that won't be retried. + */ + protected ParseException newPermanentException(int code, String message) { + ParseRequestException e = new ParseRequestException(code, message); + e.isPermanentFailure = true; + return e; + } + + /** + * Constructs a temporary exception that will be retried. + */ + protected ParseException newTemporaryException(int code, String message) { + ParseRequestException e = new ParseRequestException(code, message); + e.isPermanentFailure = false; + return e; + } + + /** + * Constructs a temporary exception that will be retried with json error code 100. + * + * @see ParseException#CONNECTION_FAILED + */ + protected ParseException newTemporaryException(String message, Throwable t) { + ParseRequestException e = new ParseRequestException( + ParseException.CONNECTION_FAILED, message, t); + e.isPermanentFailure = false; + return e; + } + + private static class ParseRequestException extends ParseException { + boolean isPermanentFailure = false; + + public ParseRequestException(int theCode, String theMessage) { + super(theCode, theMessage); + } + + public ParseRequestException(int theCode, String message, Throwable cause) { + super(theCode, message, cause); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRole.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRole.java new file mode 100644 index 0000000..c00c386 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseRole.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.regex.Pattern; + +/** + * Represents a Role on the Parse server. {@code ParseRole}s represent groupings of + * {@code ParseUsers} for the purposes of granting permissions (e.g. specifying a {@link ParseACL} + * for a {@link ParseObject}). Roles are specified by their sets of child users and child roles, all + * of which are granted any permissions that the parent role has.
+ *
+ * Roles must have a name (which cannot be changed after creation of the role), and must specify an + * ACL. + */ +@ParseClassName("_Role") +public class ParseRole extends ParseObject { + private static final Pattern NAME_PATTERN = Pattern.compile("^[0-9a-zA-Z_\\- ]+$"); + + /** + * Used for the factory methods. Developers will need to set a name on objects created like this, + * which is why the constructor with a roleName is exposed publicly. + */ + ParseRole() { + } + + /** + * Constructs a new ParseRole with the given name. If no default ACL has been specified, you must + * provide an ACL for the role. + * + * @param name + * The name of the Role to create. + */ + public ParseRole(String name) { + this(); + setName(name); + } + + /** + * Constructs a new ParseRole with the given name. + * + * @param name + * The name of the Role to create. + * @param acl + * The ACL for this role. Roles must have an ACL. + */ + public ParseRole(String name, ParseACL acl) { + this(name); + setACL(acl); + } + + /** + * Sets the name for a role. This value must be set before the role has been saved to the server, + * and cannot be set once the role has been saved.
+ *
+ * A role's name can only contain alphanumeric characters, _, -, and spaces. + * + * @param name + * The name of the role. + * @throws IllegalStateException + * if the object has already been saved to the server. + */ + public void setName(String name) { + this.put("name", name); + } + + /** + * Gets the name of the role. + * + * @return the name of the role. + */ + public String getName() { + return this.getString("name"); + } + + /** + * Gets the {@link ParseRelation} for the {@link ParseUser}s that are direct children of this + * role. These users are granted any privileges that this role has been granted (e.g. read or + * write access through ACLs). You can add or remove users from the role through this relation. + * + * @return the relation for the users belonging to this role. + */ + public ParseRelation getUsers() { + return getRelation("users"); + } + + /** + * Gets the {@link ParseRelation} for the {@link ParseRole}s that are direct children of this + * role. These roles' users are granted any privileges that this role has been granted (e.g. read + * or write access through ACLs). You can add or remove child roles from this role through this + * relation. + * + * @return the relation for the roles belonging to this role. + */ + public ParseRelation getRoles() { + return getRelation("roles"); + } + + @Override + /* package */ void validateSave() { + synchronized (mutex) { + if (this.getObjectId() == null && getName() == null) { + throw new IllegalStateException("New roles must specify a name."); + } + super.validateSave(); + } + } + + @Override + public void put(String key, Object value) { + if ("name".equals(key)) { + if (this.getObjectId() != null) { + throw new IllegalArgumentException( + "A role's name can only be set before it has been saved."); + } + if (!(value instanceof String)) { + throw new IllegalArgumentException("A role's name must be a String."); + } + if (!NAME_PATTERN.matcher((String) value).matches()) { + throw new IllegalArgumentException( + "A role's name can only contain alphanumeric characters, _, -, and spaces."); + } + } + super.put(key, value); + } + + /** + * Gets a {@link ParseQuery} over the Role collection. + * + * @return A new query over the Role collection. + */ + public static ParseQuery getQuery() { + return ParseQuery.getQuery(ParseRole.class); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteCursor.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteCursor.java new file mode 100644 index 0000000..2c542c6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteCursor.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +import bolts.Task; + +/** + * Wrapper class to invoke {@link Cursor#close()} on a specific thread on Android versions below + * android-14 as they require {@link Cursor#close()} to be called on the same thread the cursor + * was created in + * + * https://github.com/android/platform_frameworks_base/commit/6f37f83a4802a0d411395f3abc5f24a2cfec025d + */ +/** package */ class ParseSQLiteCursor implements Cursor { + + public static Cursor create(Cursor cursor, Executor executor) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return cursor; + } + return new ParseSQLiteCursor(cursor, executor); + } + + private Cursor cursor; + private Executor executor; + + private ParseSQLiteCursor(Cursor cursor, Executor executor) { + this.cursor = cursor; + this.executor = executor; + } + + @Override + public int getCount() { + return cursor.getCount(); + } + + @Override + public int getPosition() { + return cursor.getPosition(); + } + + @Override + public boolean move(int offset) { + return cursor.move(offset); + } + + @Override + public boolean moveToPosition(int position) { + return cursor.moveToPosition(position); + } + + @Override + public boolean moveToFirst() { + return cursor.moveToFirst(); + } + + @Override + public boolean moveToLast() { + return cursor.moveToLast(); + } + + @Override + public boolean moveToNext() { + return cursor.moveToNext(); + } + + @Override + public boolean moveToPrevious() { + return cursor.moveToPrevious(); + } + + @Override + public boolean isFirst() { + return cursor.isFirst(); + } + + @Override + public boolean isLast() { + return cursor.isLast(); + } + + @Override + public boolean isBeforeFirst() { + return cursor.isBeforeFirst(); + } + + @Override + public boolean isAfterLast() { + return cursor.isAfterLast(); + } + + @Override + public int getColumnIndex(String columnName) { + return cursor.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + return cursor.getColumnIndexOrThrow(columnName); + } + + @Override + public String getColumnName(int columnIndex) { + return cursor.getColumnName(columnIndex); + } + + @Override + public String[] getColumnNames() { + return cursor.getColumnNames(); + } + + @Override + public int getColumnCount() { + return cursor.getColumnCount(); + } + + @Override + public byte[] getBlob(int columnIndex) { + return cursor.getBlob(columnIndex); + } + + @Override + public String getString(int columnIndex) { + return cursor.getString(columnIndex); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + cursor.copyStringToBuffer(columnIndex, buffer); + } + + @Override + public short getShort(int columnIndex) { + return cursor.getShort(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + return cursor.getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + return cursor.getLong(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + return cursor.getFloat(columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + return cursor.getDouble(columnIndex); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public int getType(int columnIndex) { + return cursor.getType(columnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + return cursor.isNull(columnIndex); + } + + @Override + @Deprecated + public void deactivate() { + cursor.deactivate(); + } + + @Override + @Deprecated + public boolean requery() { + return cursor.requery(); + } + + @Override + public void close() { + // Basically close _eventually_. + Task.call(new Callable() { + @Override + public Void call() throws Exception { + cursor.close(); + return null; + } + }, executor); + } + + @Override + public boolean isClosed() { + return cursor.isClosed(); + } + + @Override + public void registerContentObserver(ContentObserver observer) { + cursor.registerContentObserver(observer); + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + cursor.unregisterContentObserver(observer); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + cursor.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + cursor.unregisterDataSetObserver(observer); + } + + @Override + public void setNotificationUri(ContentResolver cr, Uri uri) { + cursor.setNotificationUri(cr, uri); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public Uri getNotificationUri() { + return cursor.getNotificationUri(); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return cursor.getWantsAllOnMoveCalls(); + } + + @Override + public Bundle getExtras() { + return cursor.getExtras(); + } + + @Override + public Bundle respond(Bundle extras) { + return cursor.respond(extras); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void setExtras(Bundle bundle) { + cursor.setExtras(bundle); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteDatabase.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteDatabase.java new file mode 100644 index 0000000..c9204ab --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteDatabase.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** package */ class ParseSQLiteDatabase { + + /** + * Database connections are locked to the thread that they are created in when using transactions. + * We must use a single thread executor to make sure that all transactional DB actions are on + * the same thread or else they will block. + * + * Symptoms include blocking on db.query, cursor.moveToFirst, etc. + */ + private static final ExecutorService dbExecutor = Executors.newSingleThreadExecutor(); + + /** + * Queue for all database sessions. All database sessions must be serialized in order for + * transactions to work correctly. + */ + //TODO (grantland): do we have to serialize sessions of different databases? + private static final TaskQueue taskQueue = new TaskQueue(); + + /* protected */ static Task openDatabaseAsync(final SQLiteOpenHelper helper, int flags) { + final ParseSQLiteDatabase db = new ParseSQLiteDatabase(flags); + return db.open(helper).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return Task.forResult(db); + } + }); + } + + private SQLiteDatabase db; + private Task current = null; + private final Object currentLock = new Object(); + private final TaskCompletionSource tcs = new TaskCompletionSource<>(); + + private int openFlags; + + /** + * Creates a Session which opens a database connection and begins a transaction + */ + private ParseSQLiteDatabase(int flags) { + //TODO (grantland): if (!writable) -- disable transactions? + //TODO (grantland): if (!writable) -- do we have to serialize everything? + openFlags = flags; + + taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + synchronized (currentLock) { + current = toAwait; + } + return tcs.getTask(); + } + }); + } + + public Task isReadOnlyAsync() { + synchronized (currentLock) { + Task task = current.continueWith(new Continuation() { + @Override + public Boolean then(Task task) throws Exception { + return db.isReadOnly(); + } + }); + current = task.makeVoid(); + return task; + } + } + + public Task isOpenAsync() { + synchronized (currentLock) { + Task task = current.continueWith(new Continuation() { + @Override + public Boolean then(Task task) throws Exception { + return db.isOpen(); + } + }); + current = task.makeVoid(); + return task; + } + } + + public boolean inTransaction() { + return db.inTransaction(); + } + + /* package */ Task open(final SQLiteOpenHelper helper) { + synchronized (currentLock) { + current = current.continueWith(new Continuation() { + @Override + public SQLiteDatabase then(Task task) throws Exception { + // get*Database() is synchronous and calls through SQLiteOpenHelper#onCreate, onUpdate, + // etc. + return (openFlags & SQLiteDatabase.OPEN_READONLY) == SQLiteDatabase.OPEN_READONLY + ? helper.getReadableDatabase() + : helper.getWritableDatabase(); + } + }, dbExecutor).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + db = task.getResult(); + return task.makeVoid(); + } + }, Task.BACKGROUND_EXECUTOR); // We want to jump off the dbExecutor + return current; + } + } + + /** + * Executes a BEGIN TRANSACTION. + * @see SQLiteDatabase#beginTransaction + */ + public Task beginTransactionAsync() { + synchronized (currentLock) { + current = current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + db.beginTransaction(); + return task; + } + }, dbExecutor); + return current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Sets a transaction as successful. + * @see SQLiteDatabase#setTransactionSuccessful + */ + public Task setTransactionSuccessfulAsync() { + synchronized (currentLock) { + current = current.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + db.setTransactionSuccessful(); + return task; + } + }, dbExecutor); + return current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Ends a transaction. + * @see SQLiteDatabase#endTransaction + */ + public Task endTransactionAsync() { + synchronized (currentLock) { + current = current.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + db.endTransaction(); + // We want to swallow any exceptions from our Session task + return null; + } + }, dbExecutor); + return current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Closes this session, sets the transaction as successful if no errors occurred, ends the + * transaction and closes the database connection. + */ + public Task closeAsync() { + synchronized (currentLock) { + current = current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + try { + db.close(); + } finally { + tcs.setResult(null); + } + return tcs.getTask(); + } + }, dbExecutor); + return current.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Runs a SELECT query. + * + * @see SQLiteDatabase#query + */ + public Task queryAsync(final String table, final String[] select, final String where, + final String[] args) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Cursor then(Task task) throws Exception { + return db.query(table, select, where, args, null, null, null); + } + }, dbExecutor).onSuccess(new Continuation() { + @Override + public Cursor then(Task task) throws Exception { + Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor); + /* Ensure the cursor window is filled on the dbExecutor thread. We need to do this because + * the cursor cannot be filled from a different thread than it was created on. + */ + cursor.getCount(); + return cursor; + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Executes an INSERT. + * @see SQLiteDatabase#insertWithOnConflict + */ + public Task insertWithOnConflict(final String table, final ContentValues values, + final int conflictAlgorithm) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Long then(Task task) throws Exception { + return db.insertWithOnConflict(table, null, values, conflictAlgorithm); + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR).makeVoid(); + } + } + + /** + * Executes an INSERT and throws on SQL errors. + * @see SQLiteDatabase#insertOrThrow + */ + public Task insertOrThrowAsync(final String table, final ContentValues values) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Long then(Task task) throws Exception { + return db.insertOrThrow(table, null, values); + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR).makeVoid(); + } + } + + /** + * Executes an UPDATE. + * @see SQLiteDatabase#update + */ + public Task updateAsync(final String table, final ContentValues values, + final String where, final String[] args) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Integer then(Task task) throws Exception { + return db.update(table, values, where, args); + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } + + /** + * Executes a DELETE. + * @see SQLiteDatabase#delete + */ + public Task deleteAsync(final String table, final String where, final String[] args) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Integer then(Task task) throws Exception { + return db.delete(table, where, args); + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR).makeVoid(); + } + } + + /** + * Runs a raw query. + * + * @see SQLiteDatabase#rawQuery + */ + public Task rawQueryAsync(final String sql, final String[] args) { + synchronized (currentLock) { + Task task = current.onSuccess(new Continuation() { + @Override + public Cursor then(Task task) throws Exception { + return db.rawQuery(sql, args); + } + }, dbExecutor).onSuccess(new Continuation() { + @Override + public Cursor then(Task task) throws Exception { + Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor); + // Ensure the cursor window is filled on the dbExecutor thread. We need to do this because + // the cursor cannot be filled from a different thread than it was created on. + cursor.getCount(); + return cursor; + } + }, dbExecutor); + current = task.makeVoid(); + return task.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + // We want to jump off the dbExecutor + return task; + } + }, Task.BACKGROUND_EXECUTOR); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteOpenHelper.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteOpenHelper.java new file mode 100644 index 0000000..ba41492 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSQLiteOpenHelper.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import bolts.Task; + +/** package */ abstract class ParseSQLiteOpenHelper { + + private final SQLiteOpenHelper helper; + + public ParseSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, + int version) { + helper = new SQLiteOpenHelper(context, name, factory, version) { + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + ParseSQLiteOpenHelper.this.onOpen(db); + } + + @Override + public void onCreate(SQLiteDatabase db) { + ParseSQLiteOpenHelper.this.onCreate(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + ParseSQLiteOpenHelper.this.onUpgrade(db, oldVersion, newVersion); + } + }; + } + + public Task getReadableDatabaseAsync() { + return getDatabaseAsync(false); + } + + public Task getWritableDatabaseAsync() { + return getDatabaseAsync(true); + } + + private Task getDatabaseAsync(final boolean writable) { + return ParseSQLiteDatabase.openDatabaseAsync( + helper, !writable ? SQLiteDatabase.OPEN_READONLY : SQLiteDatabase.OPEN_READWRITE); + } + + public void onOpen(SQLiteDatabase db) { + // do nothing + } + + public abstract void onCreate(SQLiteDatabase db); + public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSession.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSession.java new file mode 100644 index 0000000..8512f48 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSession.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; + +/** + * The {@code ParseSession} is a local representation of session data that can be saved + * and retrieved from the Parse cloud. + */ +@ParseClassName("_Session") +public class ParseSession extends ParseObject { + + private static final String KEY_SESSION_TOKEN = "sessionToken"; + private static final String KEY_CREATED_WITH = "createdWith"; + private static final String KEY_RESTRICTED = "restricted"; + private static final String KEY_USER = "user"; + private static final String KEY_EXPIRES_AT = "expiresAt"; + private static final String KEY_INSTALLATION_ID = "installationId"; + + private static final List READ_ONLY_KEYS = Collections.unmodifiableList( + Arrays.asList(KEY_SESSION_TOKEN, KEY_CREATED_WITH, KEY_RESTRICTED, KEY_USER, KEY_EXPIRES_AT, + KEY_INSTALLATION_ID)); + + private static ParseSessionController getSessionController() { + return ParseCorePlugins.getInstance().getSessionController(); + } + + /** + * Get the current {@code ParseSession} object related to the current user. + * + * @return A task that resolves a {@code ParseSession} object or {@code null} if not valid or + * logged in. + */ + public static Task getCurrentSessionInBackground() { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + if (sessionToken == null) { + return Task.forResult(null); + } + return getSessionController().getSessionAsync(sessionToken).onSuccess(new Continuation() { + @Override + public ParseSession then(Task task) throws Exception { + ParseObject.State result = task.getResult(); + return ParseObject.from(result); + } + }); + } + }); + } + + /** + * Get the current {@code ParseSession} object related to the current user. + * + * @param callback A callback that returns a {@code ParseSession} object or {@code null} if not + * valid or logged in. + */ + public static void getCurrentSessionInBackground(GetCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(getCurrentSessionInBackground(), callback); + } + + /* package */ static Task revokeAsync(String sessionToken) { + if (sessionToken == null || !isRevocableSessionToken(sessionToken)) { + return Task.forResult(null); + } + return getSessionController().revokeAsync(sessionToken); + } + + /* package */ static Task upgradeToRevocableSessionAsync(String sessionToken) { + if (sessionToken == null || isRevocableSessionToken(sessionToken)) { + return Task.forResult(sessionToken); + } + + return getSessionController().upgradeToRevocable(sessionToken).onSuccess(new Continuation() { + @Override + public String then(Task task) throws Exception { + ParseObject.State result = task.getResult(); + return ParseObject.from(result).getSessionToken(); + } + }); + } + + /* package */ static boolean isRevocableSessionToken(String sessionToken) { + return sessionToken.contains("r:"); + } + + /** + * Constructs a query for {@code ParseSession}. + * + * @see com.parse.ParseQuery#getQuery(Class) + */ + public static ParseQuery getQuery() { + return ParseQuery.getQuery(ParseSession.class); + } + + @Override + /* package */ boolean needsDefaultACL() { + return false; + } + + @Override + /* package */ boolean isKeyMutable(String key) { + return !READ_ONLY_KEYS.contains(key); + } + + /** + * @return the session token for a user, if they are logged in. + */ + public String getSessionToken() { + return getString(KEY_SESSION_TOKEN); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSessionController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSessionController.java new file mode 100644 index 0000000..efeb13d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSessionController.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import bolts.Task; + +/** package */ interface ParseSessionController { + + Task getSessionAsync(String sessionToken); + + Task revokeAsync(String sessionToken); + + Task upgradeToRevocable(String sessionToken); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSetOperation.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSetOperation.java new file mode 100644 index 0000000..b122af4 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseSetOperation.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * An operation where a field is set to a given value regardless of its previous value. + */ + +import android.os.Parcel; + +/** package */ class ParseSetOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Set"; + + private final Object value; + + public ParseSetOperation(Object newValue) { + value = newValue; + } + + public Object getValue() { + return value; + } + + @Override + public Object encode(ParseEncoder objectEncoder) { + return objectEncoder.encode(value); + } + + @Override + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { + dest.writeString(OP_NAME); + parcelableEncoder.encode(value, dest); + } + + @Override + public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { + return this; + } + + @Override + public Object apply(Object oldValue, String key) { + return value; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTaskUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTaskUtils.java new file mode 100644 index 0000000..d5bdfb6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTaskUtils.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.concurrent.CancellationException; + +import bolts.AggregateException; +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** package */ class ParseTaskUtils { + + /** + * Converts a task execution into a synchronous action. + */ + //TODO (grantland): Task.cs actually throws an AggregateException if the task was cancelled with + // TaskCancellationException as an inner exception or an AggregateException with the original + // exception as an inner exception if task.isFaulted(). + // https://msdn.microsoft.com/en-us/library/dd235635(v=vs.110).aspx + /* package */ static T wait(Task task) throws ParseException { + try { + task.waitForCompletion(); + if (task.isFaulted()) { + Exception error = task.getError(); + if (error instanceof ParseException) { + throw (ParseException) error; + } + if (error instanceof AggregateException) { + throw new ParseException(error); + } + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } + throw new RuntimeException(error); + } else if (task.isCancelled()) { + throw new RuntimeException(new CancellationException()); + } + return task.getResult(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + //region Task to Callbacks + + /** + * Calls the callback after a task completes on the main thread, returning a Task that completes + * with the same result as the input task after the callback has been run. + */ + /* package */ static Task callbackOnMainThreadAsync(Task task, + final ParseCallback1 callback) { + return callbackOnMainThreadAsync(task, callback, false); + } + + /** + * Calls the callback after a task completes on the main thread, returning a Task that completes + * with the same result as the input task after the callback has been run. If reportCancellation + * is false, the callback will not be called if the task was cancelled. + */ + /* package */ static Task callbackOnMainThreadAsync(Task task, + final ParseCallback1 callback, final boolean reportCancellation) { + if (callback == null) { + return task; + } + return callbackOnMainThreadAsync(task, new ParseCallback2() { + @Override + public void done(Void aVoid, ParseException e) { + callback.done(e); + } + }, reportCancellation); + } + + + /** + * Calls the callback after a task completes on the main thread, returning a Task that completes + * with the same result as the input task after the callback has been run. + */ + /* package */ static Task callbackOnMainThreadAsync(Task task, + final ParseCallback2 callback) { + return callbackOnMainThreadAsync(task, callback, false); + } + + /** + * Calls the callback after a task completes on the main thread, returning a Task that completes + * with the same result as the input task after the callback has been run. If reportCancellation + * is false, the callback will not be called if the task was cancelled. + */ + /* package */ static Task callbackOnMainThreadAsync(Task task, + final ParseCallback2 callback, final boolean reportCancellation) { + if (callback == null) { + return task; + } + final TaskCompletionSource tcs = new TaskCompletionSource(); + task.continueWith(new Continuation() { + @Override + public Void then(final Task task) throws Exception { + if (task.isCancelled() && !reportCancellation) { + tcs.setCancelled(); + return null; + } + ParseExecutors.main().execute(new Runnable() { + @Override + public void run() { + try { + Exception error = task.getError(); + if (error != null && !(error instanceof ParseException)) { + error = new ParseException(error); + } + callback.done(task.getResult(), (ParseException) error); + } finally { + if (task.isCancelled()) { + tcs.setCancelled(); + } else if (task.isFaulted()) { + tcs.setError(task.getError()); + } else { + tcs.setResult(task.getResult()); + } + } + } + }); + return null; + } + }); + return tcs.getTask(); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTextUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTextUtils.java new file mode 100644 index 0000000..3e110eb --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTextUtils.java @@ -0,0 +1,64 @@ +/* + * 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.parse; + +/* package */ class ParseTextUtils { + + /** + * Returns a string containing the tokens joined by delimiters. + * @param tokens an array objects to be joined. Strings will be formed from + * the objects by calling object.toString(). + */ + /* package */ static String join(CharSequence delimiter, Iterable tokens) { + StringBuilder sb = new StringBuilder(); + boolean firstTime = true; + for (Object item: tokens) { + if (firstTime) { + firstTime = false; + } else { + sb.append(delimiter); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * Returns true if the string is null or 0-length. + * @param text the string to be examined + * @return true if str is null or zero length + */ + public static boolean isEmpty(CharSequence text) { + return text == null || text.length() == 0; + } + + /** + * Returns true if a and b are equal, including if they are both null. + *

Note: In platform versions 1.1 and earlier, this method only worked well if + * both the arguments were instances of String.

+ * @param a first CharSequence to check + * @param b second CharSequence to check + * @return true if a and b are equal + */ + public static boolean equals(CharSequence a, CharSequence b) { + return (a == b) || (a != null && a.equals(b)); + } + + private ParseTextUtils() { + /* cannot be instantiated */ + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTraverser.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTraverser.java new file mode 100644 index 0000000..5a8d30f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseTraverser.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Subclass ParseTraverser to make an function to be run recursively on every object pointed to on + * the given object. + */ +/** package */ abstract class ParseTraverser { + // Whether to recurse into ParseObjects that are seen. + private boolean traverseParseObjects; + + // Whether to call visit with the object passed in. + private boolean yieldRoot; + + /** + * Creates a new ParseTraverser. + */ + public ParseTraverser() { + traverseParseObjects = false; + yieldRoot = false; + } + + /** + * Override this method to implement your own functionality. + * @return true if you want the Traverser to continue. false if you want it to cancel. + */ + protected abstract boolean visit(Object object); + + /** + * Internal implementation of traverse. + */ + private void traverseInternal(Object root, boolean yieldRoot, IdentityHashMap seen) { + if (root == null || seen.containsKey(root)) { + return; + } + + if (yieldRoot) { + if (!visit(root)) { + return; + } + } + + seen.put(root, root); + + if (root instanceof JSONObject) { + JSONObject json = (JSONObject) root; + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = keys.next(); + try { + traverseInternal(json.get(key), true, seen); + } catch (JSONException e) { + // This should never happen. + throw new RuntimeException(e); + } + } + + } else if (root instanceof JSONArray) { + JSONArray array = (JSONArray) root; + for (int i = 0; i < array.length(); ++i) { + try { + traverseInternal(array.get(i), true, seen); + } catch (JSONException e) { + // This should never happen. + throw new RuntimeException(e); + } + } + + } else if (root instanceof Map) { + Map map = (Map) root; + for (Object value : map.values()) { + traverseInternal(value, true, seen); + } + + } else if (root instanceof List) { + List list = (List) root; + for (Object value : list) { + traverseInternal(value, true, seen); + } + + } else if (root instanceof ParseObject) { + if (traverseParseObjects) { + ParseObject object = (ParseObject) root; + for (String key : object.keySet()) { + traverseInternal(object.get(key), true, seen); + } + } + + } else if (root instanceof ParseACL) { + ParseACL acl = (ParseACL) root; + ParseUser user = acl.getUnresolvedUser(); + if (user != null && user.isCurrentUser()) { + traverseInternal(user, true, seen); + } + } + } + + /** + * Sets whether to recurse into ParseObjects that are seen. + * @return this to enable chaining. + */ + public ParseTraverser setTraverseParseObjects(boolean newValue) { + traverseParseObjects = newValue; + return this; + } + + /** + * Sets whether to call visit with the object passed in. + * @return this to enable chaining. + */ + public ParseTraverser setYieldRoot(boolean newValue) { + yieldRoot = newValue; + return this; + } + + /** + * Causes the traverser to traverse all objects pointed to by root, recursively. + */ + public void traverse(Object root) { + IdentityHashMap seen = new IdentityHashMap<>(); + traverseInternal(root, yieldRoot, seen); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUser.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUser.java new file mode 100644 index 0000000..1e08cc0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUser.java @@ -0,0 +1,1549 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Bundle; +import android.os.Parcel; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import bolts.Continuation; +import bolts.Task; + +/** + * The {@code ParseUser} is a local representation of user data that can be saved and retrieved from + * the Parse cloud. + */ +@ParseClassName("_User") +public class ParseUser extends ParseObject { + + private static final String KEY_SESSION_TOKEN = "sessionToken"; + private static final String KEY_AUTH_DATA = "authData"; + private static final String KEY_USERNAME = "username"; + private static final String KEY_PASSWORD = "password"; + private static final String KEY_EMAIL = "email"; + + private static final List READ_ONLY_KEYS = Collections.unmodifiableList( + Arrays.asList(KEY_SESSION_TOKEN, KEY_AUTH_DATA)); + + private static final String PARCEL_KEY_IS_CURRENT_USER = "_isCurrentUser"; + + /** + * Constructs a query for {@code ParseUser}. + * + * @see com.parse.ParseQuery#getQuery(Class) + */ + public static ParseQuery getQuery() { + return ParseQuery.getQuery(ParseUser.class); + } + + /* package for tests */ static ParseUserController getUserController() { + return ParseCorePlugins.getInstance().getUserController(); + } + + /* package for tests */ static ParseCurrentUserController getCurrentUserController() { + return ParseCorePlugins.getInstance().getCurrentUserController(); + } + + /* package for tests */ static ParseAuthenticationManager getAuthenticationManager() { + return ParseCorePlugins.getInstance().getAuthenticationManager(); + } + + /** package */ static class State extends ParseObject.State { + + /** package */ static class Builder extends Init { + + private boolean isNew; + + public Builder() { + super("_User"); + } + + /* package */ Builder(State state) { + super(state); + isNew = state.isNew(); + } + + @Override + /* package */ Builder self() { + return this; + } + + @SuppressWarnings("unchecked") + @Override + public State build() { + return new State(this); + } + + @Override + public Builder apply(ParseObject.State other) { + isNew(((State) other).isNew()); + return super.apply(other); + } + + public Builder sessionToken(String sessionToken) { + return put(KEY_SESSION_TOKEN, sessionToken); + } + + public Builder authData(Map> authData) { + return put(KEY_AUTH_DATA, authData); + } + + @SuppressWarnings("unchecked") + public Builder putAuthData(String authType, Map authData) { + Map> newAuthData = + (Map>) serverData.get(KEY_AUTH_DATA); + if (newAuthData == null) { + newAuthData = new HashMap<>(); + } + newAuthData.put(authType, authData); + serverData.put(KEY_AUTH_DATA, newAuthData); + return this; + } + + public Builder isNew(boolean isNew) { + this.isNew = isNew; + return this; + } + } + + private final boolean isNew; + + private State(Builder builder) { + super(builder); + isNew = builder.isNew; + } + + /* package */ State(Parcel source, String className, ParseParcelDecoder decoder) { + super(source, className, decoder); + isNew = source.readByte() == 1; + } + + @SuppressWarnings("unchecked") + @Override + public Builder newBuilder() { + return new Builder(this); + } + + public String sessionToken() { + return (String) get(KEY_SESSION_TOKEN); + } + + @SuppressWarnings("unchecked") + public Map> authData() { + Map> authData = + (Map>) get(KEY_AUTH_DATA); + if (authData == null) { + // We'll always return non-null for now since we don't have any null checking in place. + // Be aware not to get and set without checking size or else we'll be adding a value that + // wasn't there in the first place. + authData = new HashMap<>(); + } + return authData; + } + + public boolean isNew() { + return isNew; + } + + @Override + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + super.writeToParcel(dest, encoder); + dest.writeByte(isNew ? (byte) 1 : 0); + } + } + + // Whether the object is a currentUser. If so, it will always be persisted to disk on updates. + private boolean isCurrentUser; + + /** + * Constructs a new ParseUser with no data in it. A ParseUser constructed in this way will not + * have an objectId and will not persist to the database until {@link #signUp} is called. + */ + public ParseUser() { + isCurrentUser = false; + } + + @Override + /* package */ boolean needsDefaultACL() { + return false; + } + + @Override + boolean isKeyMutable(String key) { + return !READ_ONLY_KEYS.contains(key); + } + + @Override + /* package */ State.Builder newStateBuilder(String className) { + return new State.Builder(); + } + + @Override + /* package */ State getState() { + return (State) super.getState(); + } + + /** + * @return {@code true} if this user was created with {@link #getCurrentUser()} when no current + * user previously existed and {@link #enableAutomaticUser()} is set, false if was created by any + * other means or if a previously "lazy" user was saved remotely. + */ + /* package */ boolean isLazy() { + synchronized (mutex) { + return getObjectId() == null && ParseAnonymousUtils.isLinked(this); + } + } + + /** + * Whether the ParseUser has been authenticated on this device. This will be true if the ParseUser + * was obtained via a logIn or signUp method. Only an authenticated ParseUser can be saved (with + * altered attributes) and deleted. + */ + public boolean isAuthenticated() { + synchronized (mutex) { + ParseUser current = ParseUser.getCurrentUser(); + return isLazy() || + (getState().sessionToken() != null + && current != null + && getObjectId().equals(current.getObjectId())); + } + } + + @Override + public void remove(String key) { + if (KEY_USERNAME.equals(key)) { + throw new IllegalArgumentException("Can't remove the username key."); + } + super.remove(key); + } + + @Override + /* package */ JSONObject toRest( + ParseObject.State state, + List operationSetQueue, + ParseEncoder objectEncoder) { + // Create a sanitized copy of operationSetQueue with `password` removed if necessary + List cleanOperationSetQueue = operationSetQueue; + for (int i = 0; i < operationSetQueue.size(); i++) { + ParseOperationSet operations = operationSetQueue.get(i); + if (operations.containsKey(KEY_PASSWORD)) { + if (cleanOperationSetQueue == operationSetQueue) { + cleanOperationSetQueue = new LinkedList<>(operationSetQueue); + } + ParseOperationSet cleanOperations = new ParseOperationSet(operations); + cleanOperations.remove(KEY_PASSWORD); + cleanOperationSetQueue.set(i, cleanOperations); + } + } + return super.toRest(state, cleanOperationSetQueue, objectEncoder); + } + + /* package for tests */ Task cleanUpAuthDataAsync() { + ParseAuthenticationManager controller = getAuthenticationManager(); + Map> authData; + synchronized (mutex) { + authData = getState().authData(); + if (authData.size() == 0) { + return Task.forResult(null); // Nothing to see or do here... + } + } + + List> tasks = new ArrayList<>(); + Iterator>> i = authData.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry> entry = i.next(); + if (entry.getValue() == null) { + i.remove(); + tasks.add(controller.restoreAuthenticationAsync(entry.getKey(), null).makeVoid()); + } + } + + State newState = getState().newBuilder() + .authData(authData) + .build(); + setState(newState); + + return Task.whenAll(tasks); + } + + @Override + /* package */ Task handleSaveResultAsync( + ParseObject.State result, ParseOperationSet operationsBeforeSave) { + boolean success = result != null; + if (success) { + operationsBeforeSave.remove(KEY_PASSWORD); + } + + return super.handleSaveResultAsync(result, operationsBeforeSave); + } + + @Override + /* package */ void validateSaveEventually() throws ParseException { + if (isDirty(KEY_PASSWORD)) { + // TODO(mengyan): Unify the exception we throw when validate fails + throw new ParseException( + ParseException.OTHER_CAUSE, + "Unable to saveEventually on a ParseUser with dirty password"); + } + } + + //region Getter/Setter helper methods + + /* package */ boolean isCurrentUser() { + synchronized (mutex) { + return isCurrentUser; + } + } + + /* package */ void setIsCurrentUser(boolean isCurrentUser) { + synchronized (mutex) { + this.isCurrentUser = isCurrentUser; + } + } + + /** + * @return the session token for a user, if they are logged in. + */ + public String getSessionToken() { + return getState().sessionToken(); + } + + // This is only used when upgrading to revocable session + private Task setSessionTokenInBackground(String newSessionToken) { + synchronized (mutex) { + State state = getState(); + if (newSessionToken.equals(state.sessionToken())) { + return Task.forResult(null); + } + + State.Builder builder = state.newBuilder() + .sessionToken(newSessionToken); + setState(builder.build()); + return saveCurrentUserAsync(this); + } + } + + /* package for testes */ Map> getAuthData() { + synchronized (mutex) { + Map> authData = getMap(KEY_AUTH_DATA); + if (authData == null) { + // We'll always return non-null for now since we don't have any null checking in place. + // Be aware not to get and set without checking size or else we'll be adding a value that + // wasn't there in the first place. + authData = new HashMap<>(); + } + return authData; + } + } + + private Map getAuthData(String authType) { + return getAuthData().get(authType); + } + + /* package */ void putAuthData(String authType, Map authData) { + synchronized (mutex) { + Map> newAuthData = getAuthData(); + newAuthData.put(authType, authData); + performPut(KEY_AUTH_DATA, newAuthData); + } + } + + private void removeAuthData(String authType) { + synchronized (mutex) { + Map> newAuthData = getAuthData(); + newAuthData.remove(authType); + performPut(KEY_AUTH_DATA, newAuthData); + } + } + + /** + * Sets the username. Usernames cannot be null or blank. + * + * @param username + * The username to set. + */ + public void setUsername(String username) { + put(KEY_USERNAME, username); + } + + /** + * Retrieves the username. + */ + public String getUsername() { + return getString(KEY_USERNAME); + } + + /** + * Sets the password. + * + * @param password + * The password to set. + */ + public void setPassword(String password) { + put(KEY_PASSWORD, password); + } + + /* package for tests */ String getPassword() { + return getString(KEY_PASSWORD); + } + + /** + * Sets the email address. + * + * @param email + * The email address to set. + */ + public void setEmail(String email) { + put(KEY_EMAIL, email); + } + + /** + * Retrieves the email address. + */ + public String getEmail() { + return getString(KEY_EMAIL); + } + + /** + * Indicates whether this {@code ParseUser} was created during this session through a call to + * {@link #signUp()} or by logging in with a linked service such as Facebook. + */ + public boolean isNew() { + return getState().isNew(); + } + + //endregion + + @Override + public void put(String key, Object value) { + synchronized (mutex) { + if (KEY_USERNAME.equals(key)) { + // When the username is set, remove any semblance of anonymity. + stripAnonymity(); + } + super.put(key, value); + } + } + + private void stripAnonymity() { + synchronized (mutex) { + if (ParseAnonymousUtils.isLinked(this)) { + if (getObjectId() != null) { + putAuthData(ParseAnonymousUtils.AUTH_TYPE, null); + } else { + removeAuthData(ParseAnonymousUtils.AUTH_TYPE); + } + } + } + } + + // TODO(grantland): Can we replace this with #revert(String)? + private void restoreAnonymity(Map anonymousData) { + synchronized (mutex) { + if (anonymousData != null) { + putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousData); + } + } + } + + @Override + /* package */ void validateSave() { + synchronized (mutex) { + if (getObjectId() == null) { + throw new IllegalArgumentException( + "Cannot save a ParseUser until it has been signed up. Call signUp first."); + } + + if (isAuthenticated() || !isDirty() || isCurrentUser()) { + return; + } + } + + if (!Parse.isLocalDatastoreEnabled()) { + // This might be a different of instance of the currentUser, so we need to check objectIds + ParseUser current = ParseUser.getCurrentUser(); //TODO (grantland): possible blocking disk i/o + if (current != null && getObjectId().equals(current.getObjectId())) { + return; + } + } + + throw new IllegalArgumentException("Cannot save a ParseUser that is not authenticated."); + } + + @Override + /* package */ Task saveAsync(String sessionToken, Task toAwait) { + return saveAsync(sessionToken, isLazy(), toAwait); + } + + /* package for tests */ Task saveAsync(String sessionToken, boolean isLazy, Task toAwait) { + Task task; + if (isLazy) { + task = resolveLazinessAsync(toAwait); + } else { + task = super.saveAsync(sessionToken, toAwait); + } + + if (isCurrentUser()) { + // If the user is the currently logged in user, we persist all data to disk + return task.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return cleanUpAuthDataAsync(); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return saveCurrentUserAsync(ParseUser.this); + } + }); + } + + return task; + } + + @Override + /* package */ void setState(ParseObject.State newState) { + if (isCurrentUser()) { + State.Builder newStateBuilder = newState.newBuilder(); + // Avoid clearing sessionToken when updating the current user's State via ParseQuery result + if (getSessionToken() != null && newState.get(KEY_SESSION_TOKEN) == null) { + newStateBuilder.put(KEY_SESSION_TOKEN, getSessionToken()); + } + // Avoid clearing authData when updating the current user's State via ParseQuery result + if (getAuthData().size() > 0 && newState.get(KEY_AUTH_DATA) == null) { + newStateBuilder.put(KEY_AUTH_DATA, getAuthData()); + } + newState = newStateBuilder.build(); + } + super.setState(newState); + } + + @Override + /* package */ void validateDelete() { + synchronized (mutex) { + super.validateDelete(); + if (!isAuthenticated() && isDirty()) { + throw new IllegalArgumentException("Cannot delete a ParseUser that is not authenticated."); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public ParseUser fetch() throws ParseException { + return (ParseUser) super.fetch(); + } + + @SuppressWarnings("unchecked") + @Override + /* package */ Task fetchAsync( + String sessionToken, Task toAwait) { + //TODO (grantland): It doesn't seem like we should do this.. Why don't we error like we do + // when fetching an unsaved ParseObject? + if (isLazy()) { + return Task.forResult((T) this); + } + + Task task = super.fetchAsync(sessionToken, toAwait); + + if (isCurrentUser()) { + return task.onSuccessTask(new Continuation>() { + @Override + public Task then(final Task fetchAsyncTask) throws Exception { + return cleanUpAuthDataAsync(); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return saveCurrentUserAsync(ParseUser.this); + } + }).onSuccess(new Continuation() { + @Override + public T then(Task task) throws Exception { + return (T) ParseUser.this; + } + }); + } + + return task; + } + + /** + * Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This + * will create a new ParseUser on the server, and also persist the session on disk so that you can + * access the user using {@link #getCurrentUser}. + *

+ * A username and password must be set before calling signUp. + *

+ * This is preferable to using {@link #signUp}, unless your code is already running from a + * background thread. + * + * @return A Task that is resolved when sign up completes. + */ + public Task signUpInBackground() { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return signUpAsync(task); + } + }); + } + + /* package for tests */ Task signUpAsync(Task toAwait) { + final ParseUser user = getCurrentUser(); //TODO (grantland): convert to async + synchronized (mutex) { + final String sessionToken = user != null ? user.getSessionToken() : null; + if (ParseTextUtils.isEmpty(getUsername())) { + return Task.forError(new IllegalArgumentException("Username cannot be missing or blank")); + } + + if (ParseTextUtils.isEmpty(getPassword())) { + return Task.forError(new IllegalArgumentException("Password cannot be missing or blank")); + } + + if (getObjectId() != null) { + // For anonymous users, there may be an objectId. Setting the + // userName will have removed the anonymous link and set the + // value in the authData object to JSONObject.NULL, so we can + // just treat it like a save operation. + Map> authData = getAuthData(); + if (authData.containsKey(ParseAnonymousUtils.AUTH_TYPE) + && authData.get(ParseAnonymousUtils.AUTH_TYPE) == null) { + return saveAsync(sessionToken, toAwait); + } + + // Otherwise, throw. + return Task.forError( + new IllegalArgumentException("Cannot sign up a user that has already signed up.")); + } + + // If the operationSetQueue is has operation sets in it, then a save or signUp is in progress. + // If there is a signUp or save already in progress, don't allow another one to start. + if (operationSetQueue.size() > 1) { + return Task.forError( + new IllegalArgumentException("Cannot sign up a user that is already signing up.")); + } + + // If the current user is an anonymous user, merge this object's data into the anonymous user + // and save. + if (user != null && ParseAnonymousUtils.isLinked(user)) { + // this doesn't have any outstanding saves, so we can safely merge its operations into the + // current user. + + if (this == user) { + return Task.forError( + new IllegalArgumentException("Attempt to merge currentUser with itself.")); + } + + boolean isLazy = user.isLazy(); + final String oldUsername = user.getUsername(); + final String oldPassword = user.getPassword(); + final Map anonymousData = user.getAuthData(ParseAnonymousUtils.AUTH_TYPE); + + user.copyChangesFrom(this); + user.setUsername(getUsername()); + user.setPassword(getPassword()); + revert(); + + return user.saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isCancelled() || task.isFaulted()) { // Error + synchronized (user.mutex) { + if (oldUsername != null) { + user.setUsername(oldUsername); + } else { + user.revert(KEY_USERNAME); + } + if (oldPassword != null) { + user.setPassword(oldPassword); + } else { + user.revert(KEY_PASSWORD); + } + user.restoreAnonymity(anonymousData); + } + return task; + } else { // Success + user.revert(KEY_PASSWORD); + revert(KEY_PASSWORD); + } + + mergeFromObject(user); + return saveCurrentUserAsync(ParseUser.this); + } + }); + } + + final ParseOperationSet operations = startSave(); + + return toAwait.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getUserController().signUpAsync( + getState(), operations, sessionToken + ).continueWithTask(new Continuation>() { + @Override + public Task then(final Task signUpTask) throws Exception { + ParseUser.State result = signUpTask.getResult(); + return handleSaveResultAsync(result, + operations).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (!signUpTask.isCancelled() && !signUpTask.isFaulted()) { + return saveCurrentUserAsync(ParseUser.this); + } + return signUpTask.makeVoid(); + } + }); + } + }); + } + }); + } + } + + /** + * Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This + * will create a new ParseUser on the server, and also persist the session on disk so that you can + * access the user using {@link #getCurrentUser}. + *

+ * A username and password must be set before calling signUp. + *

+ * Typically, you should use {@link #signUpInBackground} instead of this, unless you are managing + * your own threading. + * + * @throws ParseException + * Throws an exception if the server is inaccessible, or if the username has already + * been taken. + */ + public void signUp() throws ParseException { + ParseTaskUtils.wait(signUpInBackground()); + } + + /** + * Signs up a new user. You should call this instead of {@link #save} for new ParseUsers. This + * will create a new ParseUser on the server, and also persist the session on disk so that you can + * access the user using {@link #getCurrentUser}. + *

+ * A username and password must be set before calling signUp. + *

+ * This is preferable to using {@link #signUp}, unless your code is already running from a + * background thread. + * + * @param callback + * callback.done(user, e) is called when the signUp completes. + */ + public void signUpInBackground(SignUpCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(signUpInBackground(), callback); + } + + /** + * Logs in a user with a username and password. On success, this saves the session to disk, so you + * can retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * This is preferable to using {@link #logIn}, unless your code is already running from a + * background thread. + * + * @param username + * The username to log in with. + * @param password + * The password to log in with. + * + * @return A Task that is resolved when logging in completes. + */ + public static Task logInInBackground(String username, String password) { + if (username == null) { + throw new IllegalArgumentException("Must specify a username for the user to log in with"); + } + if (password == null) { + throw new IllegalArgumentException("Must specify a password for the user to log in with"); + } + + return getUserController().logInAsync(username, password).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + State result = task.getResult(); + final ParseUser newCurrent = ParseObject.from(result); + return saveCurrentUserAsync(newCurrent).onSuccess(new Continuation() { + @Override + public ParseUser then(Task task) throws Exception { + return newCurrent; + } + }); + } + }); + } + + /** + * Logs in a user with a username and password. On success, this saves the session to disk, so you + * can retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * Typically, you should use {@link #logInInBackground} instead of this, unless you are managing + * your own threading. + * + * @param username + * The username to log in with. + * @param password + * The password to log in with. + * @throws ParseException + * Throws an exception if the login was unsuccessful. + * @return The user if the login was successful. + */ + public static ParseUser logIn(String username, String password) throws ParseException { + return ParseTaskUtils.wait(logInInBackground(username, password)); + } + + /** + * Logs in a user with a username and password. On success, this saves the session to disk, so you + * can retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * This is preferable to using {@link #logIn}, unless your code is already running from a + * background thread. + * + * @param username + * The username to log in with. + * @param password + * The password to log in with. + * @param callback + * callback.done(user, e) is called when the login completes. + */ + public static void logInInBackground(final String username, final String password, + LogInCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(logInInBackground(username, password), callback); + } + + /** + * Authorize a user with a session token. On success, this saves the session to disk, so you can + * retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * This is preferable to using {@link #become}, unless your code is already running from a + * background thread. + * + * @param sessionToken + * The session token to authorize with. + * + * @return A Task that is resolved when authorization completes. + */ + public static Task becomeInBackground(String sessionToken) { + if (sessionToken == null) { + throw new IllegalArgumentException("Must specify a sessionToken for the user to log in with"); + } + + return getUserController().getUserAsync(sessionToken).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + State result = task.getResult(); + + final ParseUser user = ParseObject.from(result); + return saveCurrentUserAsync(user).onSuccess(new Continuation() { + @Override + public ParseUser then(Task task) throws Exception { + return user; + } + }); + } + }); + } + + /** + * Authorize a user with a session token. On success, this saves the session to disk, so you can + * retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * Typically, you should use {@link #becomeInBackground} instead of this, unless you are managing + * your own threading. + * + * @param sessionToken + * The session token to authorize with. + * @throws ParseException + * Throws an exception if the authorization was unsuccessful. + * @return The user if the authorization was successful. + */ + public static ParseUser become(String sessionToken) throws ParseException { + return ParseTaskUtils.wait(becomeInBackground(sessionToken)); + } + + /** + * Authorize a user with a session token. On success, this saves the session to disk, so you can + * retrieve the currently logged in user using {@link #getCurrentUser}. + *

+ * This is preferable to using {@link #become}, unless your code is already running from a + * background thread. + * + * @param sessionToken + * The session token to authorize with. + * @param callback + * callback.done(user, e) is called when the authorization completes. + */ + public static void becomeInBackground(final String sessionToken, LogInCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(becomeInBackground(sessionToken), callback); + } + + //TODO (grantland): Publicize + /* package */ static Task getCurrentUserAsync() { + return getCurrentUserController().getAsync(); + } + + /** + * This retrieves the currently logged in ParseUser with a valid session, either from memory or + * disk if necessary. + * + * @return The currently logged in ParseUser + */ + public static ParseUser getCurrentUser() { + return getCurrentUser(isAutomaticUserEnabled()); + } + + /** + * This retrieves the currently logged in ParseUser with a valid session, either from memory or + * disk if necessary. + * + * @param shouldAutoCreateUser + * {@code true} to automatically create and set an anonymous user as current. + * @return The currently logged in ParseUser + */ + private static ParseUser getCurrentUser(boolean shouldAutoCreateUser) { + try { + return ParseTaskUtils.wait(getCurrentUserController().getAsync(shouldAutoCreateUser)); + } catch (ParseException e) { + //TODO (grantland): Publicize this exception + return null; + } + } + + //TODO (grantland): Make it throw ParseException and call #getCurrenSessionTokenInBackground() + /* package */ static String getCurrentSessionToken() { + ParseUser current = ParseUser.getCurrentUser(); + return current != null ? current.getSessionToken() : null; + } + + //TODO (grantland): Make it really async and publicize in v2 + /* package */ static Task getCurrentSessionTokenAsync() { + return getCurrentUserController().getCurrentSessionTokenAsync(); + } + + // Persists a user as currentUser to disk, and into the singleton + private static Task saveCurrentUserAsync(ParseUser user) { + return getCurrentUserController().setAsync(user); + } + + /** + * Used by {@link ParseObject#pin} to persist lazy users to LDS that haven't been pinned yet. + */ + /* package */ static Task pinCurrentUserIfNeededAsync(ParseUser user) { + if (!Parse.isLocalDatastoreEnabled()) { + throw new IllegalStateException("Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`."); + } + return getCurrentUserController().setIfNeededAsync(user); + } + + /** + * Logs out the currently logged in user session. This will remove the session from disk, log out + * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. + *

+ * This is preferable to using {@link #logOut}, unless your code is already running from a + * background thread. + * + * @return A Task that is resolved when logging out completes. + */ + public static Task logOutInBackground() { + return getCurrentUserController().logOutAsync(); + } + + /** + * Logs out the currently logged in user session. This will remove the session from disk, log out + * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. + *

+ * This is preferable to using {@link #logOut}, unless your code is already running from a + * background thread. + */ + public static void logOutInBackground(LogOutCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(logOutInBackground(), callback); + } + + /** + * Logs out the currently logged in user session. This will remove the session from disk, log out + * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. + *

+ * Typically, you should use {@link #logOutInBackground()} instead of this, unless you are + * managing your own threading. + *

+ * Note:: Any errors in the log out flow will be swallowed due to + * backward-compatibility reasons. Please use {@link #logOutInBackground()} if you'd wish to + * handle them. + */ + public static void logOut() { + try { + ParseTaskUtils.wait(logOutInBackground()); + } catch (ParseException e) { + //TODO (grantland): We shouldn't swallow errors, but we need to for backwards compatibility. + // Change this in v2. + } + } + + //TODO (grantland): Add to taskQueue + /* package */ Task logOutAsync() { + return logOutAsync(true); + } + + /* package */ Task logOutAsync(boolean revoke) { + ParseAuthenticationManager controller = getAuthenticationManager(); + List> tasks = new ArrayList<>(); + final String oldSessionToken; + + synchronized (mutex) { + oldSessionToken = getState().sessionToken(); + + for (Map.Entry> entry : getAuthData().entrySet()) { + tasks.add(controller.deauthenticateAsync(entry.getKey())); + } + + State newState = getState().newBuilder() + .sessionToken(null) + .isNew(false) + .build(); + isCurrentUser = false; + setState(newState); + } + + if (revoke) { + tasks.add(ParseSession.revokeAsync(oldSessionToken)); + } + + return Task.whenAll(tasks); + } + + /** + * Requests a password reset email to be sent in a background thread to the specified email + * address associated with the user account. This email allows the user to securely reset their + * password on the Parse site. + *

+ * This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already + * running from a background thread. + * + * @param email + * The email address associated with the user that forgot their password. + * + * @return A Task that is resolved when the command completes. + */ + public static Task requestPasswordResetInBackground(String email) { + return getUserController().requestPasswordResetAsync(email); + } + + /** + * Requests a password reset email to be sent to the specified email address associated with the + * user account. This email allows the user to securely reset their password on the Parse site. + *

+ * Typically, you should use {@link #requestPasswordResetInBackground} instead of this, unless you + * are managing your own threading. + * + * @param email + * The email address associated with the user that forgot their password. + * @throws ParseException + * Throws an exception if the server is inaccessible, or if an account with that email + * doesn't exist. + */ + public static void requestPasswordReset(String email) throws ParseException { + ParseTaskUtils.wait(requestPasswordResetInBackground(email)); + } + + /** + * Requests a password reset email to be sent in a background thread to the specified email + * address associated with the user account. This email allows the user to securely reset their + * password on the Parse site. + *

+ * This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already + * running from a background thread. + * + * @param email + * The email address associated with the user that forgot their password. + * @param callback + * callback.done(e) is called when the request completes. + */ + public static void requestPasswordResetInBackground(final String email, + RequestPasswordResetCallback callback) { + ParseTaskUtils.callbackOnMainThreadAsync(requestPasswordResetInBackground(email), callback); + } + + @SuppressWarnings("unchecked") + @Override + public ParseUser fetchIfNeeded() throws ParseException { + return super.fetchIfNeeded(); + } + + //region Third party authentication + + /** + * Registers a third party authentication callback. + *

+ * Note: This shouldn't be called directly unless developing a third party authentication + * library. + * + * @param authType The name of the third party authentication source. + * @param callback The third party authentication callback to be registered. + * + * @see AuthenticationCallback + */ + public static void registerAuthenticationCallback( + String authType, AuthenticationCallback callback) { + getAuthenticationManager().register(authType, callback); + } + + /** + * Logs in a user with third party authentication credentials. + *

+ * Note: This shouldn't be called directly unless developing a third party authentication + * library. + * + * @param authType The name of the third party authentication source. + * @param authData The user credentials of the third party authentication source. + * @return A {@code Task} is resolved when logging in completes. + * + * @see AuthenticationCallback + */ + public static Task logInWithInBackground( + final String authType, final Map authData) { + if (authType == null) { + throw new IllegalArgumentException("Invalid authType: " + null); + } + + final Continuation> logInWithTask = new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getUserController().logInAsync(authType, authData).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser.State result = task.getResult(); + final ParseUser user = ParseObject.from(result); + return saveCurrentUserAsync(user).onSuccess(new Continuation() { + @Override + public ParseUser then(Task task) throws Exception { + return user; + } + }); + } + }); + } + }; + + // Handle claiming of user. + return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser user = task.getResult(); + if (user != null) { + synchronized (user.mutex) { + if (ParseAnonymousUtils.isLinked(user)) { + if (user.isLazy()) { + final Map oldAnonymousData = + user.getAuthData(ParseAnonymousUtils.AUTH_TYPE); + return user.taskQueue.enqueue(new Continuation>() { + @Override + public Task then(final Task toAwait) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (user.mutex) { + // Replace any anonymity with the new linked authData. + user.stripAnonymity(); + user.putAuthData(authType, authData); + + return user.resolveLazinessAsync(task); + } + } + }).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (user.mutex) { + if (task.isFaulted()) { + user.removeAuthData(authType); + user.restoreAnonymity(oldAnonymousData); + return Task.forError(task.getError()); + } + if (task.isCancelled()) { + return Task.cancelled(); + } + return Task.forResult(user); + } + } + }); + } + }); + } else { + // Try to link the current user with third party user, unless a user is already linked + // to that third party user, then we'll just create a new user and link it with the + // third party user. New users will not be linked to the previous user's data. + return user.linkWithInBackground(authType, authData) + .continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.isFaulted()) { + Exception error = task.getError(); + if (error instanceof ParseException + && ((ParseException) error).getCode() == ParseException.ACCOUNT_ALREADY_LINKED) { + // An account that's linked to the given authData already exists, so log in + // instead of trying to claim. + return Task.forResult(null).continueWithTask(logInWithTask); + } + } + if (task.isCancelled()) { + return Task.cancelled(); + } + return Task.forResult(user); + } + }); + } + } + } + } + return Task.forResult(null).continueWithTask(logInWithTask); + } + }); + } + + /** + * Indicates whether this user is linked with a third party authentication source. + *

+ * Note: This shouldn't be called directly unless developing a third party authentication + * library. + * + * @param authType The name of the third party authentication source. + * @return {@code true} if linked, otherwise {@code false}. + * + * @see AuthenticationCallback + */ + public boolean isLinked(String authType) { + Map> authData = getAuthData(); + return authData.containsKey(authType) && authData.get(authType) != null; + } + + /** + * Ensures that all auth sources have auth data (e.g. access tokens, etc.) that matches this + * user. + */ + /* package */ Task synchronizeAllAuthDataAsync() { + Map> authData; + synchronized (mutex) { + if (!isCurrentUser()) { + return Task.forResult(null); + } + authData = getAuthData(); + } + List> tasks = new ArrayList<>(authData.size()); + for (String authType : authData.keySet()) { + tasks.add(synchronizeAuthDataAsync(authType)); + } + return Task.whenAll(tasks); + } + + /* package */ Task synchronizeAuthDataAsync(String authType) { + Map authData; + synchronized (mutex) { + if (!isCurrentUser()) { + return Task.forResult(null); + } + authData = getAuthData(authType); + } + return synchronizeAuthDataAsync(getAuthenticationManager(), authType, authData); + } + + private Task synchronizeAuthDataAsync( + ParseAuthenticationManager manager, final String authType, Map authData) { + return manager.restoreAuthenticationAsync(authType, authData).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + boolean success = !task.isFaulted() && task.getResult(); + if (!success) { + return unlinkFromInBackground(authType); + } + return task.makeVoid(); + } + }); + } + + private Task linkWithAsync( + final String authType, + final Map authData, + final Task toAwait, + final String sessionToken) { + synchronized (mutex) { + final boolean isLazy = isLazy(); + final Map oldAnonymousData = getAuthData(ParseAnonymousUtils.AUTH_TYPE); + + stripAnonymity(); + putAuthData(authType, authData); + + return saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + synchronized (mutex) { + if (task.isFaulted() || task.isCancelled()) { + removeAuthData(authType); + restoreAnonymity(oldAnonymousData); + return task; + } + return synchronizeAuthDataAsync(authType); + } + } + }); + } + } + + private Task linkWithAsync( + final String authType, + final Map authData, + final String sessionToken) { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return linkWithAsync(authType, authData, task, sessionToken); + } + }); + } + + /** + * Links this user to a third party authentication source. + *

+ * Note: This shouldn't be called directly unless developing a third party authentication + * library. + * + * @param authType The name of the third party authentication source. + * @param authData The user credentials of the third party authentication source. + * @return A {@code Task} is resolved when linking completes. + * + * @see AuthenticationCallback + */ + public Task linkWithInBackground( + String authType, Map authData) { + if (authType == null) { + throw new IllegalArgumentException("Invalid authType: " + null); + } + return linkWithAsync(authType, authData, getSessionToken()); + } + + /** + * Unlinks this user from a third party authentication source. + *

+ * Note: This shouldn't be called directly unless developing a third party authentication + * library. + * + * @param authType The name of the third party authentication source. + * @return A {@code Task} is resolved when unlinking completes. + * + * @see AuthenticationCallback + */ + public Task unlinkFromInBackground(final String authType) { + if (authType == null) { + return Task.forResult(null); + } + + synchronized (mutex) { + if (!getAuthData().containsKey(authType)) { + return Task.forResult(null); + } + putAuthData(authType, null); + } + + return saveInBackground(); + } + + //endregion + + /** + * Try to resolve a lazy user. + * + * If {@code authData} is empty, we'll treat this just as a SignUp. Otherwise, we'll + * treat this as a SignUpOrLogIn. We'll merge the server result with this user, only if LDS is not + * enabled. + * + * @param toAwait {@code Task} to wait for completion before running. + * @return A {@code Task} that will resolve to the current user. If this is a SignUp it'll be this + * {@code ParseUser} instance, otherwise it'll be a new {@code ParseUser} instance. + */ + /* package for tests */ Task resolveLazinessAsync(Task toAwait) { + synchronized (mutex) { + if (getAuthData().size() == 0) { // TODO(grantland): Could we just check isDirty(KEY_AUTH_DATA)? + // If there are no linked services, treat this as a SignUp. + return signUpAsync(toAwait); + } + + final ParseOperationSet operations = startSave(); + + // Otherwise, treat this as a SignUpOrLogIn + return toAwait.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return getUserController().logInAsync(getState(), operations).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + final ParseUser.State result = task.getResult(); + + Task resultTask; + // We can't merge this user with the server if this is a LogIn because LDS might + // already be keeping track of the servers objectId. + if (Parse.isLocalDatastoreEnabled() && !result.isNew()) { + resultTask = Task.forResult(result); + } else { + resultTask = handleSaveResultAsync(result, + operations).onSuccess(new Continuation() { + @Override + public ParseUser.State then(Task task) throws Exception { + return result; + } + }); + } + return resultTask.onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser.State result = task.getResult(); + if (!result.isNew()) { + // If the result is not a new user, treat this as a fresh logIn with complete + // serverData, and switch the current user to the new user. + final ParseUser newUser = ParseObject.from(result); + return saveCurrentUserAsync(newUser); + } + return task.makeVoid(); + } + }); + } + }); + } + }); + } + } + + @SuppressWarnings("unchecked") + @Override + /* package */ Task fetchFromLocalDatastoreAsync() { + // Same as #fetch() + if (isLazy()) { + return Task.forResult((T) this); + } + return super.fetchFromLocalDatastoreAsync(); + } + + //region Automatic User + + private static final Object isAutoUserEnabledMutex = new Object(); + private static boolean autoUserEnabled; + + /** + * Enables automatic creation of anonymous users. After calling this method, + * {@link #getCurrentUser()} will always have a value. The user will only be created on the server + * once the user has been saved, or once an object with a relation to that user or an ACL that + * refers to the user has been saved. + *

+ * Note: {@link ParseObject#saveEventually()} will not work if an item being + * saved has a relation to an automatic user that has never been saved. + */ + public static void enableAutomaticUser() { + synchronized (isAutoUserEnabledMutex) { + autoUserEnabled = true; + } + } + + /* package */ static void disableAutomaticUser() { + synchronized (isAutoUserEnabledMutex) { + autoUserEnabled = false; + } + } + + /* package */ static boolean isAutomaticUserEnabled() { + synchronized (isAutoUserEnabledMutex) { + return autoUserEnabled; + } + } + + //endregion + + //region Parcelable + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + synchronized (mutex) { + outState.putBoolean(PARCEL_KEY_IS_CURRENT_USER, isCurrentUser); + } + } + + @Override + protected void onRestoreInstanceState(Bundle savedState) { + super.onRestoreInstanceState(savedState); + setIsCurrentUser(savedState.getBoolean(PARCEL_KEY_IS_CURRENT_USER, false)); + } + + //endregion + + //region Legacy/Revocable Session Tokens + + /** + * Enables revocable sessions. This method is only required if you wish to use + * {@link ParseSession} APIs and do not have revocable sessions enabled in your application + * settings on your parse server. + *

+ * Upon successful completion of this {@link Task}, {@link ParseSession} APIs will be available + * for use. + * + * @return A {@link Task} that will resolve when enabling revocable session + */ + public static Task enableRevocableSessionInBackground() { + // TODO(mengyan): Right now there is no way for us to add interceptor for this client, + // so maybe we should move add interceptor steps to restClient() + ParseCorePlugins.getInstance().registerUserController( + new NetworkUserController(ParsePlugins.get().restClient(), true)); + + return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + ParseUser user = task.getResult(); + if (user == null) { + return Task.forResult(null); + } + return user.upgradeToRevocableSessionAsync(); + } + }); + } + + /* package */ Task upgradeToRevocableSessionAsync() { + return taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task toAwait) throws Exception { + return upgradeToRevocableSessionAsync(toAwait); + } + }); + } + + private Task upgradeToRevocableSessionAsync(Task toAwait) { + final String sessionToken = getSessionToken(); + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return ParseSession.upgradeToRevocableSessionAsync(sessionToken); + } + }).onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String result = task.getResult(); + return setSessionTokenInBackground(result); + } + }); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserController.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserController.java new file mode 100644 index 0000000..f0e11b6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserController.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Map; + +import bolts.Task; + +/** package */ interface ParseUserController { + + Task signUpAsync( + ParseObject.State state, + ParseOperationSet operations, + String sessionToken); + + //region logInAsync + + Task logInAsync( + String username, String password); + + Task logInAsync( + ParseUser.State state, ParseOperationSet operations); + + Task logInAsync( + String authType, Map authData); + + //endregion + + Task getUserAsync(String sessionToken); + + Task requestPasswordResetAsync(String email); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserCurrentCoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserCurrentCoder.java new file mode 100644 index 0000000..ad6cb79 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseUserCurrentCoder.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import static com.parse.ParseUser.State; + +/** + * Handles encoding/decoding ParseUser to/from /2 format JSON. /2 format json is only used for + * persisting current ParseUser and ParseInstallation to disk when LDS is not enabled. + */ +/** package */ class ParseUserCurrentCoder extends ParseObjectCurrentCoder { + + private static final String KEY_AUTH_DATA = "auth_data"; + private static final String KEY_SESSION_TOKEN = "session_token"; + + private static final ParseUserCurrentCoder INSTANCE = new ParseUserCurrentCoder(); + public static ParseUserCurrentCoder get() { + return INSTANCE; + } + + /* package */ ParseUserCurrentCoder() { + // do nothing + } + + /** + * Converts a ParseUser state to /2/ JSON representation suitable for saving to disk. + * + *

+   * {
+   *   data: {
+   *     // data fields, including objectId, createdAt, updatedAt
+   *   },
+   *   classname: class name for the object,
+   *   operations: { } // operations per field
+   * }
+   * 
+ * + * All keys are included, regardless of whether they are dirty. + * We also add sessionToken and authData to the json. + * + * @see #decode(ParseObject.State.Init, JSONObject, ParseDecoder) + */ + @Override + public JSONObject encode( + T state, ParseOperationSet operations, ParseEncoder encoder) { + + // FYI we'll be double writing sessionToken and authData for now... + // This is important. super.encode() has no notion of sessionToken and authData, so it treats them + // like objects (simply passed to the encoder). This means that a null sessionToken will become + // JSONObject.NULL. This must be accounted in #decode(). + JSONObject objectJSON = super.encode(state, operations, encoder); + + String sessionToken = ((State) state).sessionToken(); + if (sessionToken != null) { + try { + objectJSON.put(KEY_SESSION_TOKEN, sessionToken); + } catch (JSONException e) { + throw new RuntimeException("could not encode value for key: session_token"); + } + } + + Map> authData = ((State) state).authData(); + if (authData.size() > 0) { + try { + objectJSON.put(KEY_AUTH_DATA, encoder.encode(authData)); + } catch (JSONException e) { + throw new RuntimeException("could not attach key: auth_data"); + } + } + + return objectJSON; + } + + /** + * Merges from JSON in /2/ format. + * + * This is only used to read ParseUser state stored on disk in JSON. + * Since in encode we add sessionToken and authData to the json, we need remove them from json + * to generate state. + * + * @see #encode(ParseObject.State, ParseOperationSet, ParseEncoder) + */ + @Override + public > T decode( + T builder, JSONObject json, ParseDecoder decoder) { + ParseUser.State.Builder userBuilder = (State.Builder) super.decode(builder, json, decoder); + + // super.decode will read its own values and add them to the builder using put(). + // This means the state for session token and auth data might be illegal, returning + // unexpected types. For instance if sessionToken was null, now it's JSONObject.NULL. + // We must overwrite these possibly wrong values. + String newSessionToken = json.optString(KEY_SESSION_TOKEN, null); + userBuilder.sessionToken(newSessionToken); + + JSONObject newAuthData = json.optJSONObject(KEY_AUTH_DATA); + if (newAuthData == null) { + userBuilder.authData(null); + } else { + try { + @SuppressWarnings("rawtypes") + Iterator i = newAuthData.keys(); + while (i.hasNext()) { + String key = (String) i.next(); + if (!newAuthData.isNull(key)) { + userBuilder.putAuthData(key, + (Map) ParseDecoder.get().decode(newAuthData.getJSONObject(key))); + } + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + return (T) userBuilder; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseWakeLock.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseWakeLock.java new file mode 100644 index 0000000..8213fa6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ParseWakeLock.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.os.PowerManager; + +/** + * Utility class that wraps a PowerManager.WakeLock and logs an error if the app doesn't have + * permissions to acquire wake locks. + */ +/** package */ class ParseWakeLock { + private static final String TAG = "com.parse.ParseWakeLock"; + + private static volatile boolean hasWakeLockPermission = true; + private final PowerManager.WakeLock wakeLock; + + public static ParseWakeLock acquireNewWakeLock(Context context, int type, String reason, long timeout) { + PowerManager.WakeLock wl = null; + + if (hasWakeLockPermission) { + try { + PowerManager pm = (PowerManager)context.getApplicationContext().getSystemService(Context.POWER_SERVICE); + + if (pm != null) { + wl = pm.newWakeLock(type, reason); + + if (wl != null) { + wl.setReferenceCounted(false); + + if (timeout == 0) { + wl.acquire(); + } else { + wl.acquire(timeout); + } + } + } + } catch (SecurityException e) { + PLog.e(TAG, "Failed to acquire a PowerManager.WakeLock. This is" + + "necessary for reliable handling of pushes. Please add this to your Manifest.xml: " + + " "); + + hasWakeLockPermission = false; + wl = null; + } + } + + return new ParseWakeLock(wl); + } + + private ParseWakeLock(PowerManager.WakeLock wakeLock) { + this.wakeLock = wakeLock; + } + + public void release() { + if (wakeLock != null) { + wakeLock.release(); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerEncoder.java new file mode 100644 index 0000000..3f6ba83 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerEncoder.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; + +/** + * Encodes {@link ParseObjects} as pointers. If the object does not have an objectId, throws an + * exception. + */ +/** package */ class PointerEncoder extends PointerOrLocalIdEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final PointerEncoder INSTANCE = new PointerEncoder(); + public static PointerEncoder get() { + return INSTANCE; + } + + @Override + public JSONObject encodeRelatedObject(ParseObject object) { + // Ensure the ParseObject has an id so it can be encoded as a pointer. + if (object.getObjectId() == null) { + // object that hasn't been saved. + throw new IllegalStateException("unable to encode an association with an unsaved ParseObject"); + } + return super.encodeRelatedObject(object); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java new file mode 100644 index 0000000..e9b41f1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encodes {@link ParseObjects} as pointers. If the object does not have an objectId, uses a + * local id. + */ +/** package */ class PointerOrLocalIdEncoder extends ParseEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final PointerOrLocalIdEncoder INSTANCE = new PointerOrLocalIdEncoder(); + public static PointerOrLocalIdEncoder get() { + return INSTANCE; + } + + @Override + public JSONObject encodeRelatedObject(ParseObject object) { + JSONObject json = new JSONObject(); + try { + if (object.getObjectId() != null) { + json.put("__type", "Pointer"); + json.put("className", object.getClassName()); + json.put("objectId", object.getObjectId()); + } else { + json.put("__type", "Pointer"); + json.put("className", object.getClassName()); + json.put("localId", object.getOrCreateLocalId()); + } + } catch (JSONException e) { + // This should not happen + throw new RuntimeException(e); + } + return json; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ProgressCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ProgressCallback.java new file mode 100644 index 0000000..18f760c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/ProgressCallback.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code ProgressCallback} is used to get upload or download progress of a {@link ParseFile} + * action. + *

+ * The easiest way to use a {@code ProgressCallback} is through an anonymous inner class. + */ +// FYI, this does not extend ParseCallback2 since it does not match the usual signature +// done(T, ParseException), but is done(T). +public interface ProgressCallback { + /** + * Override this function with your desired callback. + */ + void done(Integer percentDone); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHandler.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHandler.java new file mode 100644 index 0000000..576e003 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Intent; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import org.json.JSONObject; + +import java.util.List; + +import bolts.Task; + +/** + * An interface for for handling push payloads (or any similar events, e.g. registration) + * that woke up the {@link com.parse.PushService}. + * Each subclass represent a certain {@link com.parse.PushType}. + * + * These classes are short-lived, instantiated at the moment of handling a push payload + * or initializing. They should not be 'stateful' in this sense. + */ +/** package */ interface PushHandler { + + // TODO: let someone extend this somehow if we want to publicize handlers + class Factory { + static PushHandler create(PushType type) { + switch (type) { + case GCM: return new GcmPushHandler(); + case NONE: return new FallbackHandler(); + } + return null; + } + } + + enum SupportLevel { + /* + * Manifest has all required and optional declarations necessary to support this push service. + */ + SUPPORTED, + + /* + * Manifest has all required declarations to support this push service, but is missing some + * optional declarations. + */ + MISSING_OPTIONAL_DECLARATIONS, + + /* + * Manifest doesn't have enough required declarations to support this push service. + */ + MISSING_REQUIRED_DECLARATIONS + } + + + /** + * Whether this push handler is supported by the current device and manifest configuration. + * Implementors can parse the manifest file using utilities in {@link ManifestInfo}. + * @return true if supported + */ + @NonNull + SupportLevel isSupported(); + + /** + * Returns a warning message to be shown in logs, depending on the support level returned + * by {@link #isSupported()}. Called on the same instance. + */ + @Nullable + String getWarningMessage(SupportLevel level); + + /** + * If this handler is the default handler for this device, + * initialize is called to let it set up things, launch registrations intents, + * or whatever else is needed. + * + * This method is also responsible to update the current {@link ParseInstallation} + * fields to let parse server work (e.g. device token, push type). + * + * @return a task completing when init completed + */ + Task initialize(); + + /** + * Handle a raw intent. + * This is called in a background thread so can be synchronous. + * Handlers can do checks over the intent and then dispatch the push notification to + * {@link ParsePushBroadcastReceiver}, by calling + * {@link PushRouter#handlePush(String, String, String, JSONObject)}. + */ + @WorkerThread + void handlePush(Intent intent); + + + class FallbackHandler implements PushHandler { + + private FallbackHandler() {}; + + @Nullable + @Override + public String getWarningMessage(SupportLevel level) { + return null; + } + + @NonNull + @Override + public SupportLevel isSupported() { + return SupportLevel.SUPPORTED; + } + + @Override + public Task initialize() { + return Task.forResult(null); + } + + @Override + public void handlePush(Intent intent) {} + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHistory.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHistory.java new file mode 100644 index 0000000..56fcd8c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushHistory.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.PriorityQueue; + +/** + * PushHistory manages a fixed-length history of pushes received. It is used by to dedup recently + * received messages, as well as keep track of a last received timestamp that is included in PPNS + * handshakes. + */ +/** package */ class PushHistory { + private static final String TAG = "com.parse.PushHistory"; + + private static class Entry implements Comparable { + public String pushId; + public String timestamp; + + public Entry(String pushId, String timestamp) { + this.pushId = pushId; + this.timestamp = timestamp; + } + + @Override + public int compareTo(Entry other) { + return timestamp.compareTo(other.timestamp); + } + } + + private final int maxHistoryLength; + private final PriorityQueue entries; + private final HashSet pushIds; + private String lastTime; + + /** + * Creates a push history object from a JSON object that looks like this: + * + * { + * "seen": { + * "push_id_1": "2013-11-01T22:01:00.000Z", + * "push_id_2": "2013-11-01T22:01:01.000Z", + * "push_id_3": "2013-11-01T22:01:02.000Z" + * }, + * "lastTime": "2013-11-01T22:01:02.000Z" + * } + * + * The "history" entries correspond to entries in the "entries" queue. + * The "lastTime" entry corresponds to the "lastTime" field. + */ + public PushHistory(int maxHistoryLength, JSONObject json) { + this.maxHistoryLength = maxHistoryLength; + this.entries = new PriorityQueue<>(maxHistoryLength + 1); + this.pushIds = new HashSet<>(maxHistoryLength + 1); + this.lastTime = null; + + if (json != null) { + JSONObject jsonHistory = json.optJSONObject("seen"); + if (jsonHistory != null) { + Iterator it = jsonHistory.keys(); + while (it.hasNext()) { + String pushId = it.next(); + String timestamp = jsonHistory.optString(pushId, null); + + if (pushId != null && timestamp != null) { + tryInsertPush(pushId, timestamp); + } + } + } + setLastReceivedTimestamp(json.optString("lastTime", null)); + } + } + + /** + * Serializes the history state to a JSON object using the format described in loadJSON(). + */ + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + + if (entries.size() > 0) { + JSONObject history = new JSONObject(); + for (Entry e : entries) { + history.put(e.pushId, e.timestamp); + } + json.put("seen", history); + } + + json.putOpt("lastTime", lastTime); + + return json; + } + + /** + * Returns the last received timestamp, which is always updated whether or not a push was + * successfully inserted into history. + */ + public String getLastReceivedTimestamp() { + return lastTime; + } + + public void setLastReceivedTimestamp(String lastTime) { + this.lastTime = lastTime; + } + + /** + * Attempts to insert a push into history. The push is ignored if we have already seen it + * recently. Otherwise, the push is inserted into history. If the length of the history exceeds + * the maximum length, then the history is trimmed by removing the oldest pushes until it no + * longer exceeds the maximum length. + * + * @return Returns whether or not the push was inserted into history. + */ + public boolean tryInsertPush(String pushId, String timestamp) { + if (timestamp == null) { + throw new IllegalArgumentException("Can't insert null pushId or timestamp into history"); + } + + if (lastTime == null || timestamp.compareTo(lastTime) > 0) { + lastTime = timestamp; + } + + if (pushIds.contains(pushId)) { + PLog.e(TAG, "Ignored duplicate push " + pushId); + return false; + } + + entries.add(new Entry(pushId, timestamp)); + pushIds.add(pushId); + + while (entries.size() > maxHistoryLength) { + Entry head = entries.remove(); + pushIds.remove(head.pushId); + } + + return true; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushRouter.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushRouter.java new file mode 100644 index 0000000..3630e89 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushRouter.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +/** + * PushRouter handles distribution of push payloads through a broadcast intent with the + * "com.parse.push.intent.RECEIVE" action. It also serializes a history of the last several pushes + * seen by this app. This history is necessary for two reasons: + * + * - For PPNS, we provide the last-seen timestamp to the server as part of the handshake. This is + * used as a cursor into the server-side inbox of recent pushes for this client. + * - For GCM, we use the history to deduplicate pushes when GCM decides to change the canonical + * registration id for a client (which can result in duplicate pushes while both the old and + * new registration id are still valid). + */ +/** package */ class PushRouter { + private static final String TAG = "com.parse.ParsePushRouter"; + private static final String LEGACY_STATE_LOCATION = "pushState"; + private static final String STATE_LOCATION = "push"; + private static final int MAX_HISTORY_LENGTH = 10; + + private static PushRouter instance; + public static synchronized PushRouter getInstance() { + if (instance == null) { + File diskState = new File(ParsePlugins.get().getFilesDir(), STATE_LOCATION); + File oldDiskState = new File(ParsePlugins.get().getParseDir(), LEGACY_STATE_LOCATION); + instance = pushRouterFromState(diskState, oldDiskState, MAX_HISTORY_LENGTH); + } + + return instance; + } + + /* package for tests */ static synchronized void resetInstance() { + ParseFileUtils.deleteQuietly(new File(ParsePlugins.get().getFilesDir(), STATE_LOCATION)); + instance = null; + } + + /* package for tests */ static PushRouter pushRouterFromState( + File diskState, File oldDiskState, int maxHistoryLength) { + JSONObject state = readJSONFileQuietly(diskState); + JSONObject historyJSON = (state != null) ? state.optJSONObject("history") : null; + PushHistory history = new PushHistory(maxHistoryLength, historyJSON); + + // If the deserialized push history object doesn't have a last timestamp, we might have to + // migrate the last timestamp from the legacy pushState file instead. + boolean didMigrate = false; + if (history.getLastReceivedTimestamp() == null) { + JSONObject oldState = readJSONFileQuietly(oldDiskState); + if (oldState != null) { + String lastTime = oldState.optString("lastTime", null); + if (lastTime != null) { + history.setLastReceivedTimestamp(lastTime); + } + didMigrate = true; + } + } + + PushRouter router = new PushRouter(diskState, history); + + if (didMigrate) { + router.saveStateToDisk(); + ParseFileUtils.deleteQuietly(oldDiskState); + } + + return router; + } + + private static JSONObject readJSONFileQuietly(File file) { + JSONObject json = null; + if (file != null) { + try { + json = ParseFileUtils.readFileToJSONObject(file); + } catch (IOException | JSONException e) { + // do nothing + } + } + return json; + } + + private final File diskState; + private final PushHistory history; + + private PushRouter(File diskState, PushHistory history) { + this.diskState = diskState; + this.history = history; + } + + /** + * Returns the state in this object as a persistable JSONObject. The persisted state looks like + * this: + * + * { + * "history": { + * "seen": { + * "": "", + * ... + * } + * "lastTime": "" + * } + * } + */ + /* package */ synchronized JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("history", history.toJSON()); + return json; + } + + private synchronized void saveStateToDisk() { + try { + ParseFileUtils.writeJSONObjectToFile(diskState, toJSON()); + } catch (IOException | JSONException e) { + PLog.e(TAG, "Unexpected error when serializing push state to " + diskState, e); + } + } + + public synchronized String getLastReceivedTimestamp() { + return history.getLastReceivedTimestamp(); + } + + public synchronized boolean handlePush( + String pushId, String timestamp, String channel, JSONObject data) { + if (ParseTextUtils.isEmpty(pushId) || ParseTextUtils.isEmpty(timestamp)) { + return false; + } + + if (!history.tryInsertPush(pushId, timestamp)) { + return false; + } + + // Persist the fact that we've seen this push. + saveStateToDisk(); + + Bundle extras = new Bundle(); + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_CHANNEL, channel); + if (data == null) { + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, "{}"); + } else { + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, data.toString()); + } + + Intent intent = new Intent(ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE); + intent.putExtras(extras); + + // Set the package name to keep this intent within the given package. + Context context = Parse.getApplicationContext(); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent); + + return true; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushService.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushService.java new file mode 100644 index 0000000..f7de87a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushService.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.PowerManager; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A service to listen for push notifications. This operates in the same process as the parent + * application. + *

+ * The {@code PushService} can listen to pushes from Google Cloud Messaging (GCM). + * To configure the {@code PushService} for GCM, ensure these permission declarations are present in + * your AndroidManifest.xml as children of the <manifest> element: + *

+ *

+ * <uses-permission android:name="android.permission.INTERNET" />
+ * <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ * <uses-permission android:name="android.permission.VIBRATE" />
+ * <uses-permission android:name="android.permission.WAKE_LOCK" />
+ * <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ * <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+ * <permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE"
+ *   android:protectionLevel="signature" />
+ * <uses-permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE" />
+ * 
+ *

+ * Replace YOUR_PACKAGE_NAME in the declarations above with your application's package name. Also, + * make sure that {@link GcmBroadcastReceiver}, {@link PushService} and + * {@link ParsePushBroadcastReceiver} are declared as children of the + * <application> element: + *

+ *

+ * <service android:name="com.parse.PushService" />
+ * <receiver android:name="com.parse.GcmBroadcastReceiver"
+ *  android:permission="com.google.android.c2dm.permission.SEND">
+ *   <intent-filter>
+ *     <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ *     <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+ *     <category android:name="YOUR_PACKAGE_NAME" />
+ *   </intent-filter>
+ * </receiver>
+ * <receiver android:name="com.parse.ParsePushBroadcastReceiver" android:exported=false>
+ *  <intent-filter>
+ *     <action android:name="com.parse.push.intent.RECEIVE" />
+ *     <action android:name="com.parse.push.intent.OPEN" />
+ *     <action android:name="com.parse.push.intent.DELETE" />
+ *   </intent-filter>
+ * </receiver>
+ * 
+ *

+ * Again, replace YOUR_PACKAGE_NAME with your application's package name. + * If you want to customize the way your app generates Notifications for your pushes, you + * can register a custom subclass of {@link ParsePushBroadcastReceiver}. + *

+ * Once push notifications are configured in the manifest, you can subscribe to a push channel by + * calling: + *

+ *

+ * ParsePush.subscribeInBackground("the_channel_name");
+ * 
+ *

+ * When the client receives a push message, a notification will appear in the system tray. When the + * user taps the notification, it will broadcast the "com.parse.push.intent.OPEN" intent. + * The {@link ParsePushBroadcastReceiver} listens to this intent to track an app open event and + * launch the app's launcher activity. To customize this behavior override + * {@link ParsePushBroadcastReceiver#onPushOpen(Context, Intent)}. + * + * Starting with Android O, this is replaced by {@link PushServiceApi26}. + */ +public final class PushService extends Service { + private static final String TAG = "com.parse.PushService"; + + //region run and dispose + + private static final String WAKE_LOCK_EXTRA = "parseWakeLockId"; + private static final SparseArray wakeLocks = new SparseArray<>(); + private static int wakeLockId = 0; + + /* + * Same as Context.startService, but acquires a wake lock before starting the service. The wake + * lock must later be released by calling dispose(). + */ + static boolean run(Context context, Intent intent) { + String reason = intent.toString(); + ParseWakeLock wl = ParseWakeLock.acquireNewWakeLock(context, PowerManager.PARTIAL_WAKE_LOCK, reason, 0); + + synchronized (wakeLocks) { + intent.putExtra(WAKE_LOCK_EXTRA, wakeLockId); + wakeLocks.append(wakeLockId, wl); + wakeLockId++; + } + + intent.setClass(context, PushService.class); + ComponentName name = context.startService(intent); + if (name == null) { + PLog.e(TAG, "Could not start the service. Make sure that the XML tag " + + " is in your " + + "AndroidManifest.xml as a child of the element."); + dispose(intent); + return false; + } + return true; + } + + static void dispose(Intent intent) { + if (intent != null && intent.hasExtra(WAKE_LOCK_EXTRA)) { + int id = intent.getIntExtra(WAKE_LOCK_EXTRA, -1); + ParseWakeLock wakeLock; + + synchronized (wakeLocks) { + wakeLock = wakeLocks.get(id); + wakeLocks.remove(id); + } + + if (wakeLock == null) { + PLog.e(TAG, "Got wake lock id of " + id + " in intent, but no such lock found in " + + "global map. Was disposePushService called twice for the same intent?"); + } else { + wakeLock.release(); + } + } + } + + //region ServiceLifecycleCallbacks used for testing + + private static List serviceLifecycleCallbacks = null; + + /* package */ interface ServiceLifecycleCallbacks { + void onServiceCreated(Service service); + void onServiceDestroyed(Service service); + } + + /* package */ static void registerServiceLifecycleCallbacks(ServiceLifecycleCallbacks callbacks) { + synchronized (PushService.class) { + if (serviceLifecycleCallbacks == null) { + serviceLifecycleCallbacks = new ArrayList<>(); + } + serviceLifecycleCallbacks.add(callbacks); + } + } + + /* package */ static void unregisterServiceLifecycleCallbacks(ServiceLifecycleCallbacks callbacks) { + synchronized (PushService.class) { + serviceLifecycleCallbacks.remove(callbacks); + } + } + + private static void dispatchOnServiceCreated(Service service) { + if (serviceLifecycleCallbacks != null) { + for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) { + callback.onServiceCreated(service); + } + } + } + + private static void dispatchOnServiceDestroyed(Service service) { + if (serviceLifecycleCallbacks != null) { + for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) { + callback.onServiceDestroyed(service); + } + } + } + + //endregion + + // We delegate the intent to a PushHandler running in a streamlined executor. + private ExecutorService executor; + private PushHandler handler; + + /** + * Client code should not construct a PushService directly. + */ + public PushService() { + super(); + } + + // For tests + void setPushHandler(PushHandler handler) { + this.handler = handler; + } + + /** + * Called at startup at the moment of parsing the manifest, to see + * if it was correctly set-up. + */ + static boolean isSupported() { + return ManifestInfo.getServiceInfo(PushService.class) != null; + } + + + /** + * Client code should not call {@code onCreate} directly. + */ + @Override + public void onCreate() { + super.onCreate(); + if (ParsePlugins.get() == null) { + PLog.e(TAG, "The Parse push service cannot start because Parse.initialize " + + "has not yet been called. If you call Parse.initialize from " + + "an Activity's onCreate, that call should instead be in the " + + "Application.onCreate. Be sure your Application class is registered " + + "in your AndroidManifest.xml with the android:name property of your " + + " tag."); + stopSelf(); + return; + } + + executor = Executors.newSingleThreadExecutor(); + handler = PushServiceUtils.createPushHandler(); + dispatchOnServiceCreated(this); + } + + /** + * Client code should not call {@code onStartCommand} directly. + */ + @Override + public int onStartCommand(final Intent intent, int flags, final int startId) { + if (ManifestInfo.getPushType() == PushType.NONE) { + PLog.e(TAG, "Started push service even though no push service is enabled: " + intent); + } + + executor.execute(new Runnable() { + @Override + public void run() { + try { + handler.handlePush(intent); + } finally { + dispose(intent); + stopSelf(startId); + } + } + }); + + return START_NOT_STICKY; + } + + /** + * Client code should not call {@code onBind} directly. + */ + @Override + public IBinder onBind(Intent intent) { + throw new IllegalArgumentException("You cannot bind directly to the PushService. " + + "Use PushService.subscribe instead."); + } + + /** + * Client code should not call {@code onDestroy} directly. + */ + @Override + public void onDestroy() { + if (executor != null) { + executor.shutdown(); + executor = null; + handler = null; + } + + dispatchOnServiceDestroyed(this); + super.onDestroy(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceApi26.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceApi26.java new file mode 100644 index 0000000..cf4633e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceApi26.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A JobService that is triggered by push notifications on Oreo+. + * Read {@link PushServiceUtils} and {@link PushService} for info and docs. + * This is already set-up in our own manifest. + */ +@TargetApi(Build.VERSION_CODES.O) +public final class PushServiceApi26 extends JobService { + private static final String TAG = PushServiceApi26.class.getSimpleName(); + private static final String INTENT_KEY = "intent"; + private static final int JOB_SERVICE_ID = 999; + + static boolean run(Context context, Intent intent) { + JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + // Execute in the next second. + Bundle extra = new Bundle(1); + extra.putParcelable(INTENT_KEY, intent); + ComponentName component = new ComponentName(context, PushServiceApi26.class); + int did = scheduler.schedule(new JobInfo.Builder(JOB_SERVICE_ID, component) + .setMinimumLatency(1L) + .setOverrideDeadline(1000L) + .setRequiresCharging(false) + .setRequiresBatteryNotLow(false) + .setRequiresStorageNotLow(false) + .setTransientExtras(extra) + .build()); + return did == JobScheduler.RESULT_SUCCESS; + } + + // We delegate the intent to a PushHandler running in a streamlined executor. + private ExecutorService executor; + private PushHandler handler; + private int jobsCount; + + // Our manifest file is OK. + static boolean isSupported() { + return true; + } + + @Override + public boolean onStartJob(final JobParameters jobParameters) { + if (ParsePlugins.get() == null) { + PLog.e(TAG, "The Parse push service cannot start because Parse.initialize " + + "has not yet been called. If you call Parse.initialize from " + + "an Activity's onCreate, that call should instead be in the " + + "Application.onCreate. Be sure your Application class is registered " + + "in your AndroidManifest.xml with the android:name property of your " + + " tag."); + return false; + } + + final Bundle params = jobParameters.getTransientExtras(); + final Intent intent = params.getParcelable(INTENT_KEY); + jobsCount++; + getExecutor().execute(new Runnable() { + @Override + public void run() { + try { + getHandler().handlePush(intent); + } finally { + jobFinished(jobParameters, false); + jobsCount--; + if (jobsCount == 0) { + tearDown(); + } + } + } + }); + return true; + } + + @Override + public boolean onStopJob(JobParameters jobParameters) { + // Something went wrong before jobFinished(). Try rescheduling. + return true; + } + + private Executor getExecutor() { + if (executor == null) executor = Executors.newSingleThreadExecutor(); + return executor; + } + + private PushHandler getHandler() { + if (handler == null) handler = PushServiceUtils.createPushHandler(); + return handler; + } + + private void tearDown() { + if (executor != null) executor.shutdown(); + executor = null; + handler = null; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceUtils.java new file mode 100644 index 0000000..5b8b5d1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushServiceUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.annotation.NonNull; + +import bolts.Task; + + +/** + * Helper class mostly used to access and wake the push dispatching class, plus some other utilities. + * + * Android O introduces limitations over Context.startService. If the app is currently considered + * in background, the call will result in a crash. The only reliable solutions are either using + * Context.startServiceInForeground, which does not fit our case, or move to the JobScheduler + * engine, which is what we do here for Oreo, launching {@link PushServiceApi26}. + * + * Pre-oreo, we just launch {@link PushService}. + * + * See: + * https://developer.android.com/about/versions/oreo/background.html + * https://developer.android.com/reference/android/support/v4/content/WakefulBroadcastReceiver.html + */ +abstract class PushServiceUtils { + private static final boolean USE_JOBS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + /** + * Wakes the PushService class by running it either as a Service or as a scheduled job + * depending on API level. + * + * @param context calling context + * @param intent non-null intent to be passed to the PushHandlers + * @return true if service could be launched + */ + public static boolean runService(Context context, @NonNull Intent intent) { + if (USE_JOBS) { + return PushServiceApi26.run(context, intent); + } else { + return PushService.run(context, intent); + } + } + + // Checks the manifest file. + static boolean isSupported() { + if (USE_JOBS) { + return PushServiceApi26.isSupported(); + } else { + return PushService.isSupported(); + } + } + + // Some handlers might need initialization. + static Task initialize() { + return createPushHandler().initialize(); + } + + static PushHandler createPushHandler() { + return PushHandler.Factory.create(ManifestInfo.getPushType()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushType.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushType.java new file mode 100644 index 0000000..7edcacd --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/PushType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.List; + +/** package */ enum PushType { + NONE("none"), + GCM("gcm"); + + private final String pushType; + + PushType(String pushType) { + this.pushType = pushType; + } + + static PushType fromString(String pushType) { + if ("none".equals(pushType)) { + return PushType.NONE; + } else if ("gcm".equals(pushType)) { + return PushType.GCM; + } else { + return null; + } + } + + @Override + public String toString() { + return pushType; + } + + // Preference ordered list. + // TODO: let someone inject here if we want public handlers + static PushType[] types() { + return new PushType[]{ GCM, NONE }; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RefreshCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RefreshCallback.java new file mode 100644 index 0000000..7561228 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RefreshCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code RefreshCallback} is used to run code after refresh is used to update a {@link ParseObject} in a + * background thread. + *

+ * The easiest way to use a {@code RefreshCallback} is through an anonymous inner class. Override + * the {@code done} function to specify what the callback should do after the refresh is complete. + * The {@code done} function will be run in the UI thread, while the refresh happens in a + * background thread. This ensures that the UI does not freeze while the refresh happens. + *

+ * For example, this sample code refreshes an object of class {@code "MyClass"} and id + * {@code myId}. It calls a different function depending on whether the refresh succeeded or + * not. + *

+ *

+ * object.refreshInBackground(new RefreshCallback() {
+ *   public void done(ParseObject object, ParseException e) {
+ *     if (e == null) {
+ *       objectWasRefreshedSuccessfully(object);
+ *     } else {
+ *       objectRefreshFailed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface RefreshCallback extends ParseCallback2 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param object + * The object that was refreshed, or {@code null} if it did not succeed. + * @param e + * The exception raised by the login, or {@code null} if it succeeded. + */ + @Override + void done(ParseObject object, ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RequestPasswordResetCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RequestPasswordResetCallback.java new file mode 100644 index 0000000..54d1259 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/RequestPasswordResetCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code RequestPasswordResetCallback} is used to run code requesting a password reset for a + * user. + *

+ * The easiest way to use a {@code RequestPasswordResetCallback} is through an anonymous inner + * class. Override the {@code done} function to specify what the callback should do after the + * request is complete. The {@code done} function will be run in the UI thread, while the request + * happens in a background thread. This ensures that the UI does not freeze while the request + * happens. + *

+ * For example, this sample code requests a password reset for a user and calls a different function + * depending on whether the request succeeded or not. + *

+ *

+ * ParseUser.requestPasswordResetInBackground("forgetful@example.com",
+ *     new RequestPasswordResetCallback() {
+ *       public void done(ParseException e) {
+ *         if (e == null) {
+ *           requestedSuccessfully();
+ *         } else {
+ *           requestDidNotSucceed();
+ *         }
+ *       }
+ *     });
+ * 
+ */ +public interface RequestPasswordResetCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the request is complete. + * + * @param e + * The exception raised by the save, or {@code null} if no account is associated with the + * email address. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SaveCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SaveCallback.java new file mode 100644 index 0000000..239890e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SaveCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code SaveCallback} is used to run code after saving a {@link ParseObject} in a background + * thread. + *

+ * The easiest way to use a {@code SaveCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the save is complete. The + * {@code done} function will be run in the UI thread, while the save happens in a background + * thread. This ensures that the UI does not freeze while the save happens. + *

+ * For example, this sample code saves the object {@code myObject} and calls a different + * function depending on whether the save succeeded or not. + *

+ *

+ * myObject.saveInBackground(new SaveCallback() {
+ *   public void done(ParseException e) {
+ *     if (e == null) {
+ *       myObjectSavedSuccessfully();
+ *     } else {
+ *       myObjectSaveDidNotSucceed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface SaveCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param e + * The exception raised by the save, or {@code null} if it succeeded. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SendCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SendCallback.java new file mode 100644 index 0000000..afe2ecd --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SendCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code SendCallback} is used to run code after sending a {@link ParsePush} in a background + * thread. + *

+ * The easiest way to use a {@code SendCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the send is complete. The + * {@code done} function will be run in the UI thread, while the send happens in a background + * thread. This ensures that the UI does not freeze while the send happens. + *

+ * For example, this sample code sends the message {@code "Hello world"} on the + * {@code "hello"} channel and logs whether the send succeeded. + *

+ *

+ * ParsePush push = new ParsePush();
+ * push.setChannel("hello");
+ * push.setMessage("Hello world!");
+ * push.sendInBackground(new SendCallback() {
+ *   public void done(ParseException e) {
+ *     if (e == null) {
+ *       Log.d("push", "success!");
+ *     } else {
+ *       Log.d("push", "failure");
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface SendCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the send is complete. + * + * @param e + * The exception raised by the send, or {@code null} if it succeeded. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SignUpCallback.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SignUpCallback.java new file mode 100644 index 0000000..6f3cd7c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/SignUpCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +/** + * A {@code SignUpCallback} is used to run code after signing up a {@link ParseUser} in a background + * thread. + *

+ * The easiest way to use a {@code SignUpCallback} is through an anonymous inner class. Override the + * {@code done} function to specify what the callback should do after the save is complete. The + * {@code done} function will be run in the UI thread, while the signup happens in a background + * thread. This ensures that the UI does not freeze while the signup happens. + *

+ * For example, this sample code signs up the object {@code myUser} and calls a different + * function depending on whether the signup succeeded or not. + *

+ * + *

+ * myUser.signUpInBackground(new SignUpCallback() {
+ *   public void done(ParseException e) {
+ *     if (e == null) {
+ *       myUserSignedUpSuccessfully();
+ *     } else {
+ *       myUserSignUpDidNotSucceed();
+ *     }
+ *   }
+ * });
+ * 
+ */ +public interface SignUpCallback extends ParseCallback1 { + /** + * Override this function with the code you want to run after the signUp is complete. + * + * @param e + * The exception raised by the signUp, or {@code null} if it succeeded. + */ + @Override + void done(ParseException e); +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskQueue.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskQueue.java new file mode 100644 index 0000000..7425cd2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskQueue.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.Arrays; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import bolts.Continuation; +import bolts.Task; + +/** + * A helper class for enqueueing tasks + */ +/** package */ class TaskQueue { + /** + * We only need to keep the tail of the queue. Cancelled tasks will just complete + * normally/immediately when their turn arrives. + */ + private Task tail; + private final Lock lock = new ReentrantLock(); + + /** + * Gets a task that can be safely awaited and is dependent on the current tail of the queue. This + * essentially gives us a proxy for the tail end of the queue that can be safely cancelled. + * + * @return A new task that should be awaited by enqueued tasks. + */ + private Task getTaskToAwait() { + lock.lock(); + try { + Task toAwait = tail != null ? tail : Task. forResult(null); + return toAwait.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + return null; + } + }); + } finally { + lock.unlock(); + } + } + + /** + * Enqueues a task created by taskStart. + * + * @param taskStart + * A function given a task to await once state is snapshotted (e.g. after capturing + * session tokens at the time of the save call). Awaiting this task will wait for the + * created task's turn in the queue. + * @return The task created by the taskStart function. + */ + Task enqueue(Continuation> taskStart) { + lock.lock(); + try { + Task task; + Task oldTail = tail != null ? tail : Task. forResult(null); + // The task created by taskStart is responsible for waiting for the task passed into it before + // doing its work (this gives it an opportunity to do startup work or save state before + // waiting for its turn in the queue) + try { + Task toAwait = getTaskToAwait(); + task = taskStart.then(toAwait); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + + // The tail task should be dependent on the old tail as well as the newly-created task. This + // prevents cancellation of the new task from causing the queue to run out of order. + tail = Task.whenAll(Arrays.asList(oldTail, task)); + return task; + } finally { + lock.unlock(); + } + } + + /** + * Creates a continuation that will wait for the given task to complete before running the next + * continuations. + */ + static Continuation> waitFor(final Task toAwait) { + return new Continuation>() { + @Override + public Task then(final Task task) throws Exception { + return toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task ignored) throws Exception { + return task; + } + }); + } + }; + } + + Lock getLock() { + return lock; + } + + void waitUntilFinished() throws InterruptedException { + lock.lock(); + try { + if (tail == null) { + return; + } + tail.waitForCompletion(); + } finally { + lock.unlock(); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskStackBuilderHelper.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskStackBuilderHelper.java new file mode 100644 index 0000000..2e63014 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/TaskStackBuilderHelper.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.TaskStackBuilder; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +/** + * This is here to avoid the dependency on the android support library. + * TaskStackBuilder was introduced in API 11, so in order to eliminate warnings of the type + * 'Could not find class...' this takes advantage of lazy class loading. + * TODO (pdjones): make more similar to support-v4 api + */ +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +/* package */ class TaskStackBuilderHelper { + public static void startActivities(Context context, Class cls, Intent activityIntent) { + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(cls); + stackBuilder.addNextIntent(activityIntent); + stackBuilder.startActivities(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/WeakValueHashMap.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/WeakValueHashMap.java new file mode 100644 index 0000000..230c519 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/WeakValueHashMap.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + +/** + * A HashMap where all the values are weak. + */ +/** package */ class WeakValueHashMap { + private HashMap> map; + + public WeakValueHashMap() { + map = new HashMap<>(); + } + + public void put(K key, V value) { + map.put(key, new WeakReference<>(value)); + } + + /** + * Returns null if the key isn't in the map, or if it is an expired reference. If it is, then the + * reference is removed from the map. + */ + public V get(K key) { + WeakReference reference = map.get(key); + if (reference == null) { + return null; + } + + V value = reference.get(); + if (value == null) { + map.remove(key); + } + + return value; + } + + public void remove(K key) { + map.remove(key); + } + + public void clear() { + map.clear(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpBody.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpBody.java new file mode 100644 index 0000000..463032c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpBody.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The base interface of a http body. It can be implemented by different http libraries such as + * Apache http, Android URLConnection, Square OKHttp and so on. + */ +public abstract class ParseHttpBody { + + private final String contentType; + private final long contentLength; + + /** + * Returns the content of this body. + * + * @return The content of this body. + * @throws IOException + * Throws an exception if the content of this body is inaccessible. + */ + public abstract InputStream getContent() throws IOException; + + /** + * Writes the content of this request to {@code out}. + * + * @param out + * The outputStream the content of this body needs to be written to. + * @throws IOException + * Throws an exception if the content of this body can not be written to {@code out}. + */ + public abstract void writeTo(OutputStream out) throws IOException; + + /** + * Creates an {@code ParseHttpBody} with given {@code Content-Type} and {@code Content-Length}. + * + * @param contentType + * The {@code Content-Type} of the {@code ParseHttpBody}. + * @param contentLength + * The {@code Content-Length} of the {@code ParseHttpBody}. + */ + public ParseHttpBody(String contentType, long contentLength) { + this.contentType = contentType; + this.contentLength = contentLength; + } + + /** + * Returns the number of bytes which will be written to {@code out} when {@link #writeTo} is + * called, or {@code -1} if that count is unknown. + * + * @return The Content-Length of this body. + */ + public long getContentLength() { + return contentLength; + } + + /** + * Returns the {@code Content-Type} of this body. + * + * @return The {@code Content-Type} of this body. + */ + public String getContentType() { + return contentType; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpRequest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpRequest.java new file mode 100644 index 0000000..fc012d1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpRequest.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse.http; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The http request we send to parse server. Instances of this class are not immutable. The + * request body may be consumed only once. The other fields are immutable. + */ +public final class ParseHttpRequest { + + /** + * The {@code ParseHttpRequest} method type. + */ + public enum Method { + + GET, POST, PUT, DELETE; + + /** + * Creates a {@code Method} from the given string. Valid stings are {@code GET}, {@code POST}, + * {@code PUT} and {@code DELETE}. + * + * @param string + * The string value of this {@code Method}. + * @return A {@code Method} based on the given string. + */ + public static Method fromString(String string) { + Method method; + switch (string) { + case "GET": + method = GET; + break; + case "POST": + method = POST; + break; + case "PUT": + method = PUT; + break; + case "DELETE": + method = DELETE; + break; + default: + throw new IllegalArgumentException("Invalid http method: <" + string + ">"); + } + return method; + } + + /** + * Returns a string value of this {@code Method}. + * @return The string value of this {@code Method}. + */ + @Override + public String toString() { + String string; + switch (this) { + case GET: + string = "GET"; + break; + case POST: + string = "POST"; + break; + case PUT: + string = "PUT"; + break; + case DELETE: + string = "DELETE"; + break; + default: + throw new IllegalArgumentException("Invalid http method: <" + this+ ">"); + } + return string; + } + } + + /** + * Builder of {@code ParseHttpRequest}. + */ + public static final class Builder { + + private String url; + private Method method; + private Map headers; + private ParseHttpBody body; + + /** + * Creates an empty {@code Builder}. + */ + public Builder() { + this.headers = new HashMap<>(); + } + + /** + * Creates a new {@code Builder} based on the given {@code ParseHttpRequest}. + * + * @param request + * The {@code ParseHttpRequest} where the {@code Builder}'s values come from. + */ + public Builder(ParseHttpRequest request) { + this.url = request.url; + this.method = request.method; + this.headers = new HashMap<>(request.headers); + this.body = request.body; + } + + /** + * Sets the url of this {@code Builder}. + * + * @param url + * The url of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setUrl(String url) { + this.url = url; + return this; + } + + /** + * Sets the {@link com.parse.http.ParseHttpRequest.Method} of this {@code Builder}. + * + * @param method + * The {@link com.parse.http.ParseHttpRequest.Method} of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setMethod(ParseHttpRequest.Method method) { + this.method = method; + return this; + } + + /** + * Sets the {@link ParseHttpBody} of this {@code Builder}. + * + * @param body + * The {@link ParseHttpBody} of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setBody(ParseHttpBody body) { + this.body = body; + return this; + } + + /** + * Adds a header to this {@code Builder}. + * + * @param name + * The name of the header. + * @param value + * The value of the header. + * @return This {@code Builder}. + */ + public Builder addHeader(String name, String value) { + headers.put(name, value); + return this; + } + + /** + * Adds headers to this {@code Builder}. + * + * @param headers + * The headers that need to be added. + * @return This {@code Builder}. + */ + public Builder addHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Sets headers of this {@code Builder}. All existing headers will be cleared. + * + * @param headers + * The headers of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setHeaders(Map headers) { + this.headers = new HashMap<>(headers); + return this; + } + + /** + * Builds a {@link ParseHttpRequest} based on this {@code Builder}. + * + * @return A {@link ParseHttpRequest} built on this {@code Builder}. + */ + public ParseHttpRequest build() { + return new ParseHttpRequest(this); + } + } + + private final String url; + private final Method method; + private final Map headers; + private final ParseHttpBody body; + + private ParseHttpRequest(Builder builder) { + this.url = builder.url; + this.method = builder.method; + this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); + this.body = builder.body; + } + + /** + * Gets the url of this {@code ParseHttpRequest}. + * + * @return The url of this {@code ParseHttpRequest}. + */ + public String getUrl() { + return url; + } + + /** + * Gets the {@code Method} of this {@code ParseHttpRequest}. + * + * @return The {@code Method} of this {@code ParseHttpRequest}. + */ + public Method getMethod() { + return method; + } + + /** + * Gets all headers from this {@code ParseHttpRequest}. + * + * @return The headers of this {@code ParseHttpRequest}. + */ + public Map getAllHeaders() { + return headers; + } + + /** + * Retrieves the header value from this {@code ParseHttpRequest} by the given header name. + * + * @param name + * The name of the header. + * @return The value of the header. + */ + public String getHeader(String name) { + return headers.get(name); + } + + /** + * Gets http body of this {@code ParseHttpRequest}. + * + * @return The http body of this {@code ParseHttpRequest}. + */ + public ParseHttpBody getBody() { + return body; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpResponse.java b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpResponse.java new file mode 100644 index 0000000..cf402fb --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/http/ParseHttpResponse.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse.http; + +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The http response we receive from parse server. Instances of this class are not immutable. The + * response body may be consumed only once. The other fields are immutable. + */ +public final class ParseHttpResponse { + + /** + * Builder for {@code ParseHttpResponse}. + */ + public static final class Builder { + + private int statusCode; + private InputStream content; + private long totalSize; + private String reasonPhrase; + private Map headers; + private String contentType; + + /** + * Creates an empty {@code Builder}. + */ + public Builder() { + this.totalSize = -1; + this.headers = new HashMap<>(); + } + + /** + * Makes a new {@code Builder} based on the given {@code ParseHttpResponse}. + * + * @param response + * The {@code ParseHttpResponse} where the {@code Builder}'s values come from. + */ + public Builder(ParseHttpResponse response) { + super(); + this.setStatusCode(response.getStatusCode()); + this.setContent(response.getContent()); + this.setTotalSize(response.getTotalSize()); + this.setContentType(response.getContentType()); + this.setHeaders(response.getAllHeaders()); + this.setReasonPhrase(response.getReasonPhrase()); + } + + /** + * Sets the status code of this {@code Builder}. + * + * @param statusCode + * The status code of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setStatusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + /** + * Sets the content of this {@code Builder}. + * + * @param content + * The content of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setContent(InputStream content) { + this.content = content; + return this; + } + + /** + * Sets the total size of this {@code Builder}. + * + * @param totalSize + * The total size of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setTotalSize(long totalSize) { + this.totalSize = totalSize; + return this; + } + + /** + * Sets the reason phrase of this {@code Builder}. + * + * @param reasonPhrase + * The reason phrase of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setReasonPhrase(String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + return this; + } + + /** + * Sets headers of this {@code Builder}. All existing headers will be cleared. + * + * @param headers + * The headers of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setHeaders(Map headers) { + this.headers = new HashMap<>(headers); + return this; + } + + /** + * Adds headers to this {@code Builder}. + * + * @param headers + * The headers that need to be added. + * @return This {@code Builder}. + */ + public Builder addHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + /** + * Adds a header to this {@code Builder}. + * + * @param name + * The name of the header. + * @param value + * The value of the header. + * @return This {@code Builder}. + */ + public Builder addHeader(String name, String value) { + headers.put(name, value); + return this; + } + + /** + * Sets the content type of this {@code Builder}. + * + * @param contentType + * The {@code Content-Type} of this {@code Builder}. + * @return This {@code Builder}. + */ + public Builder setContentType(String contentType) { + this.contentType = contentType; + return this; + } + + /** + * Builds a {@link ParseHttpResponse} by this {@code Builder}. + * + * @return A {@link ParseHttpResponse} built on this {@code Builder}. + */ + public ParseHttpResponse build() { + return new ParseHttpResponse(this); + } + } + + private final int statusCode; + private final InputStream content; + private final long totalSize; + private final String reasonPhrase; + private final Map headers; + private final String contentType; + + private ParseHttpResponse(Builder builder) { + this.statusCode = builder.statusCode; + this.content = builder.content; + this.totalSize = builder.totalSize; + this.reasonPhrase = builder.reasonPhrase; + this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); + this.contentType = builder.contentType; + } + + /** + * Gets the status code of this {@code ParseHttpResponse}. + * + * @return The status code of this {@code ParseHttpResponse}. + */ + public int getStatusCode() { + return statusCode; + } + + /** + * Returns the content of the {@code ParseHttpResponse}'s body. The content can only + * be read once and can't be reset. + * + * @return The content of the {@code ParseHttpResponse}'s body. + */ + public InputStream getContent() { + return content; + } + + /** + * Returns the size of the {@code ParseHttpResponse}'s body. {@code -1} if the size of the + * {@code ParseHttpResponse}'s body is unknown. + * + * @return The size of the {@code ParseHttpResponse}'s body. + */ + public long getTotalSize() { + return totalSize; + } + + /** + * Gets the reason phrase of this {@code ParseHttpResponse}. + * + * @return The reason phrase of this {@code ParseHttpResponse}. + */ + public String getReasonPhrase() { + return reasonPhrase; + } + + /** + * Gets the {@code Content-Type} of this {@code ParseHttpResponse}. + * + * @return The {@code Content-Type} of this {@code ParseHttpResponse}. + */ + public String getContentType() { + return contentType; + } + + /** + * Retrieves the header value from this {@code ParseHttpResponse} by the given header name. + * + * @param name + * The name of the header. + * @return The value of the header. + */ + public String getHeader(String name) { + return headers.get(name); + } + + /** + * Gets all headers from this {@code ParseHttpResponse}. + * + * @return The headers of this {@code ParseHttpResponse}. + */ + public Map getAllHeaders() { + return headers; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java new file mode 100644 index 0000000..2f1902a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import bolts.Task; +import bolts.TaskCompletionSource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CachedCurrentInstallationControllerTest { + + private static final String KEY_DEVICE_TYPE = "deviceType"; + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseInstallation.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseInstallation.class); + } + + //region testSetAsync + + @Test + public void testSetAsyncWithNotCurrentInstallation() throws Exception { + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(null, null); + + ParseInstallation currentInstallationInMemory = mock(ParseInstallation.class); + controller.currentInstallation = currentInstallationInMemory; + ParseInstallation testInstallation = mock(ParseInstallation.class); + + ParseTaskUtils.wait(controller.setAsync(testInstallation)); + + // Make sure the in memory currentInstallation not change + assertSame(currentInstallationInMemory, controller.currentInstallation); + assertNotSame(controller.currentInstallation, testInstallation); + } + + @Test + public void testSetAsyncWithCurrentInstallation() throws Exception { + InstallationId installationId = mock(InstallationId.class); + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, installationId); + ParseInstallation currentInstallation = mock(ParseInstallation.class); + when(currentInstallation.getInstallationId()).thenReturn("testInstallationId"); + controller.currentInstallation = currentInstallation; + + ParseTaskUtils.wait(controller.setAsync(currentInstallation)); + + // Verify that we persist it + verify(store, times(1)).setAsync(currentInstallation); + // Make sure installationId is updated + verify(installationId, times(1)).set("testInstallationId"); + } + + //endregion + + //region testGetAsync + + @Test + public void testGetAsyncFromMemory() throws Exception { + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(null, null); + + ParseInstallation currentInstallationInMemory = new ParseInstallation(); + controller.currentInstallation = currentInstallationInMemory; + + ParseInstallation currentInstallation = ParseTaskUtils.wait(controller.getAsync()); + + assertSame(currentInstallationInMemory, currentInstallation); + } + + @Test + public void testGetAsyncFromStore() throws Exception { + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + ParseInstallation installation = mock(ParseInstallation.class); + when(installation.getInstallationId()).thenReturn("testInstallationId"); + when(store.getAsync()).thenReturn(Task.forResult(installation)); + + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, installationId); + + ParseInstallation currentInstallation = ParseTaskUtils.wait(controller.getAsync()); + + verify(store, times(1)).getAsync(); + // Make sure installationId is updated + verify(installationId, times(1)).set("testInstallationId"); + // Make sure controller state is update to date + assertSame(installation, controller.currentInstallation); + // Make sure the installation we get is correct + assertSame(installation, currentInstallation); + } + + @Test + public void testGetAsyncWithNoInstallation() throws Exception { + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + when(installationId.get()).thenReturn("testInstallationId"); + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(null)); + + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, installationId); + + ParseInstallation currentInstallation = ParseTaskUtils.wait(controller.getAsync()); + + verify(store, times(1)).getAsync(); + // Make sure controller state is update to date + assertSame(controller.currentInstallation, currentInstallation); + // Make sure device info is updated + assertEquals("testInstallationId", currentInstallation.getInstallationId()); + assertEquals("android", currentInstallation.get(KEY_DEVICE_TYPE)); + } + + @Test + public void testGetAsyncWithNoInstallationRaceCondition() throws ParseException { + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + when(installationId.get()).thenReturn("testInstallationId"); + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + TaskCompletionSource tcs = new TaskCompletionSource(); + when(store.getAsync()).thenReturn(tcs.getTask()); + + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, installationId); + + Task taskA = controller.getAsync(); + Task taskB = controller.getAsync(); + + tcs.setResult(null); + ParseInstallation installationA = ParseTaskUtils.wait(taskA); + ParseInstallation installationB = ParseTaskUtils.wait(taskB); + + verify(store, times(1)).getAsync(); + assertSame(controller.currentInstallation, installationA); + assertSame(controller.currentInstallation, installationB); + // Make sure device info is updated + assertEquals("testInstallationId", installationA.getInstallationId()); + assertEquals("android", installationA.get(KEY_DEVICE_TYPE)); + } + + //endregion + + //region testExistsAsync + + @Test + public void testExistAsyncFromMemory() throws Exception { + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(null, null); + controller.currentInstallation = mock(ParseInstallation.class); + + assertTrue(ParseTaskUtils.wait(controller.existsAsync())); + } + + @Test + public void testExistAsyncFromStore() throws Exception { + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + when(store.existsAsync()).thenReturn(Task.forResult(true)); + + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, null); + + assertTrue(ParseTaskUtils.wait(controller.existsAsync())); + verify(store, times(1)).existsAsync(); + } + + //endregion + + @Test + public void testClearFromMemory() throws Exception { + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(null, null); + controller.currentInstallation = mock(ParseInstallation.class); + + controller.clearFromMemory(); + + assertNull(controller.currentInstallation); + } + + @Test + public void testClearFromDisk() throws Exception { + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + //noinspection unchecked + ParseObjectStore store = mock(ParseObjectStore.class); + when(store.deleteAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(store, installationId); + controller.currentInstallation = mock(ParseInstallation.class); + + controller.clearFromDisk(); + + assertNull(controller.currentInstallation); + // Make sure the in LDS currentInstallation is cleared + verify(store, times(1)).deleteAsync(); + // Make sure installationId is cleared + verify(installationId, times(1)).clear(); + } + + @Test + public void testIsCurrent() throws Exception { + // Create test controller + CachedCurrentInstallationController controller = + new CachedCurrentInstallationController(null, null); + ParseInstallation installation = mock(ParseInstallation.class); + controller.currentInstallation = installation; + + assertTrue(controller.isCurrent(installation)); + assertFalse(controller.isCurrent(new ParseInstallation())); + } +} \ No newline at end of file diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java new file mode 100644 index 0000000..3087466 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class CachedCurrentUserControllerTest extends ResetPluginsParseTest { + + private static final String KEY_AUTH_DATA = "authData"; + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseObject.registerSubclass(ParseUser.class); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + ParseObject.unregisterSubclass(ParseUser.class); + } + + //region testSetAsync + + @Test + public void testSetAsyncWithOldInMemoryCurrentUser() throws Exception { + // Mock currentUser in memory + ParseUser oldCurrentUser = mock(ParseUser.class); + when(oldCurrentUser.logOutAsync(anyBoolean())).thenReturn(Task.forResult(null)); + + ParseUser.State state = new ParseUser.State.Builder() + .put("key", "value") + .build(); + ParseUser currentUser = ParseObject.from(state); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.setAsync(currentUser)).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + controller.currentUser = oldCurrentUser; + + ParseTaskUtils.wait(controller.setAsync(currentUser)); + + // Make sure oldCurrentUser logout + verify(oldCurrentUser, times(1)).logOutAsync(false); + // Verify it was persisted + verify(store, times(1)).setAsync(currentUser); + // TODO(mengyan): Find a way to verify user.synchronizeAllAuthData() is called + // Verify newUser is currentUser + assertTrue(currentUser.isCurrentUser()); + // Make sure in memory currentUser is up to date + assertSame(currentUser, controller.currentUser); + assertTrue(controller.currentUserMatchesDisk); + } + + @Test + public void testSetAsyncWithNoInMemoryCurrentUser() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .put("key", "value") + .build(); + ParseUser currentUser = ParseObject.from(state); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.setAsync(currentUser)).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseTaskUtils.wait(controller.setAsync(currentUser)); + + // Verify it was persisted + verify(store, times(1)).setAsync(currentUser); + // TODO(mengyan): Find a way to verify user.synchronizeAllAuthData() is called + // Verify newUser is currentUser + assertTrue(currentUser.isCurrentUser()); + // Make sure in memory currentUser is up to date + assertSame(currentUser, controller.currentUser); + assertTrue(controller.currentUserMatchesDisk); + } + + @Test + public void testSetAsyncWithPersistFailure() throws Exception { + // Mock currentUser in memory + ParseUser oldCurrentUser = mock(ParseUser.class); + when(oldCurrentUser.logOutAsync(anyBoolean())).thenReturn(Task.forResult(null)); + + ParseUser currentUser = new ParseUser(); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.setAsync(currentUser)) + .thenReturn(Task.forError(new RuntimeException("failure"))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + controller.currentUser = oldCurrentUser; + + ParseTaskUtils.wait(controller.setAsync(currentUser)); + + // Make sure oldCurrentUser logout + verify(oldCurrentUser, times(1)).logOutAsync(false); + // Verify we tried to persist + verify(store, times(1)).setAsync(currentUser); + // TODO(mengyan): Find a way to verify user.synchronizeAllAuthData() is called + // Verify newUser is currentUser + assertTrue(currentUser.isCurrentUser()); + // Make sure in memory currentUser is up to date + assertSame(currentUser, controller.currentUser); + // Make sure in currentUserMatchesDisk since we can not write to disk + assertFalse(controller.currentUserMatchesDisk); + } + + //endregion + + //region testGetAsync + + @Test + public void testGetAsyncWithInMemoryCurrentUserSet() throws Exception { + ParseUser currentUserInMemory = new ParseUser(); + + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + controller.currentUser = currentUserInMemory; + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(false)); + + assertSame(currentUserInMemory, currentUser); + } + + @Test + public void testGetAsyncWithNoInMemoryCurrentUserAndLazyLogin() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + ParseCorePlugins.getInstance().registerCurrentUserController(controller); + // CurrentUser is null but currentUserMatchesDisk is true happens when a user logout + controller.currentUserMatchesDisk = true; + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(true)); + + // We need to make sure the user is created by lazy login + assertTrue(currentUser.isLazy()); + assertTrue(currentUser.isCurrentUser()); + assertSame(controller.currentUser, currentUser); + assertFalse(controller.currentUserMatchesDisk); + // We do not test the lazy login auth data here, it is covered in lazyLogin() unit test + } + + @Test + public void testGetAsyncWithNoInMemoryAndInDiskCurrentUserAndNoLazyLogin() + throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + // CurrentUser is null but currentUserMatchesDisk is true happens when a user logout + controller.currentUserMatchesDisk = true; + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(false)); + + assertNull(currentUser); + } + + @Test + public void testGetAsyncWithCurrentUserReadFromDiskSuccess() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .put("key", "value") + .build(); + ParseUser currentUserInDisk = ParseObject.from(state); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(currentUserInDisk)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(false)); + + assertSame(currentUser, currentUserInDisk); + assertSame(currentUser, controller.currentUser); + assertTrue(controller.currentUserMatchesDisk); + assertTrue(currentUser.isCurrentUser()); + assertEquals("value", currentUser.get("key")); + } + + @Test + public void testGetAsyncAnonymousUser() throws Exception{ + ParseUser.State state = new ParseUser.State.Builder() + .objectId("fake") + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .build(); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(ParseObject.from(state))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseUser user = ParseTaskUtils.wait(controller.getAsync(false)); + assertFalse(user.isLazy()); + } + + @Test + public void testGetAsyncLazyAnonymousUser() throws Exception{ + ParseUser.State state = new ParseUser.State.Builder() + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .build(); + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(ParseObject.from(state))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseUser user = ParseTaskUtils.wait(controller.getAsync(false)); + assertTrue(user.isLazy()); + } + + @Test + public void testGetAsyncWithCurrentUserReadFromDiskFailure() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(false)); + + assertNull(currentUser); + } + + @Test + public void testGetAsyncWithCurrentUserReadFromDiskFailureAndLazyLogin() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseUser currentUser = ParseTaskUtils.wait(controller.getAsync(true)); + + // We need to make sure the user is created by lazy login + assertTrue(currentUser.isLazy()); + assertTrue(currentUser.isCurrentUser()); + assertSame(controller.currentUser, currentUser); + assertFalse(controller.currentUserMatchesDisk); + // We do not test the lazy login auth data here, it is covered in lazyLogin() unit test + } + + //endregion + + //region testLogoOutAsync + + @Test + public void testLogOutAsyncWithDeleteInDiskCurrentUserSuccess() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.deleteAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + // We set the currentUser to make sure getAsync() return a mock user + ParseUser currentUser = mock(ParseUser.class); + when(currentUser.logOutAsync()).thenReturn(Task.forResult(null)); + controller.currentUser = currentUser; + + ParseTaskUtils.wait(controller.logOutAsync()); + + // Make sure currentUser.logout() is called + verify(currentUser, times(1)).logOutAsync(); + // Make sure in disk currentUser is deleted + verify(store, times(1)).deleteAsync(); + // Make sure controller state is correct + assertNull(controller.currentUser); + assertTrue(controller.currentUserMatchesDisk); + } + + @Test + public void testLogOutAsyncWithDeleteInDiskCurrentUserFailure() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.deleteAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + ParseTaskUtils.wait(controller.logOutAsync()); + + // Make sure controller state is correct + assertNull(controller.currentUser); + assertFalse(controller.currentUserMatchesDisk); + } + + //endregion + + //region testLazyLogin + + @Test + public void testLazyLogin() throws Exception { + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + + String authType = ParseAnonymousUtils.AUTH_TYPE; + Map authData = new HashMap<>(); + authData.put("sessionToken", "testSessionToken"); + + ParseUser user = controller.lazyLogIn(authType, authData); + + // Make sure use is generated through lazyLogin + assertTrue(user.isLazy()); + assertTrue(user.isCurrentUser()); + Map> authPair = user.getMap(KEY_AUTH_DATA); + assertEquals(1, authPair.size()); + Map authDataAgain = authPair.get(authType); + assertEquals(1, authDataAgain.size()); + assertEquals("testSessionToken", authDataAgain.get("sessionToken")); + // Make sure controller state is correct + assertSame(user, controller.currentUser); + assertFalse(controller.currentUserMatchesDisk); + } + + //endregion + + //region testGetCurrentSessionTokenAsync + + @Test + public void testGetCurrentSessionTokenAsyncWithCurrentUserSet() throws Exception { + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + + // We set the currentUser to make sure getAsync() return a mock user + ParseUser currentUser = mock(ParseUser.class); + when(currentUser.getSessionToken()).thenReturn("sessionToken"); + controller.currentUser = currentUser; + + String sessionToken = ParseTaskUtils.wait(controller.getCurrentSessionTokenAsync()); + + assertEquals("sessionToken", sessionToken); + } + + @Test + public void testGetCurrentSessionTokenAsyncWithNoCurrentUserSet() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.getAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + String sessionToken = ParseTaskUtils.wait(controller.getCurrentSessionTokenAsync()); + + assertNull(sessionToken); + } + + //endregion + + //region testClearFromMemory + + @Test + public void testClearFromMemory() throws Exception { + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + controller.currentUser = mock(ParseUser.class); + + controller.clearFromMemory(); + + assertNull(controller.currentUser); + assertFalse(controller.currentUserMatchesDisk); + } + + //endregion + + //region testClearFromDisk() + + @Test + public void testClearFromDisk() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.deleteAsync()).thenReturn(Task.forResult(null)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + controller.currentUser = new ParseUser(); + + controller.clearFromDisk(); + + assertNull(controller.currentUser); + assertFalse(controller.currentUserMatchesDisk); + verify(store, times(1)).deleteAsync(); + } + + //endregion + + //region testExistsAsync() + + @Test + public void testExistsAsyncWithInMemoryCurrentUserSet() throws Exception { + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + controller.currentUser = new ParseUser(); + + assertTrue(ParseTaskUtils.wait(controller.existsAsync())); + } + + @Test + public void testExistsAsyncWithInDiskCurrentUserSet() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.existsAsync()).thenReturn(Task.forResult(true)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + assertTrue(ParseTaskUtils.wait(controller.existsAsync())); + } + + @Test + public void testExistsAsyncWithNoInMemoryAndInDiskCurrentUserSet() throws Exception { + ParseObjectStore store = + (ParseObjectStore) mock(ParseObjectStore.class); + when(store.existsAsync()).thenReturn(Task.forResult(false)); + + CachedCurrentUserController controller = + new CachedCurrentUserController(store); + + assertFalse(ParseTaskUtils.wait(controller.existsAsync())); + } + + //endregion + + //region testIsCurrent + + @Test + public void testIsCurrent() throws Exception { + CachedCurrentUserController controller = + new CachedCurrentUserController(null); + ParseUser currentUser = new ParseUser(); + controller.currentUser = currentUser; + + assertTrue(controller.isCurrent(currentUser)); + assertFalse(controller.isCurrent(new ParseUser())); + } + + //endregion +} \ No newline at end of file diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/FileObjectStoreTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/FileObjectStoreTest.java new file mode 100644 index 0000000..f7c6443 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/FileObjectStoreTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +public class FileObjectStoreTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseUser.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseUser.class); + } + + @Test + public void testSetAsync() throws Exception { + File file = new File(temporaryFolder.getRoot(), "test"); + + ParseUser.State state = mock(ParseUser.State.class); + JSONObject json = new JSONObject(); + json.put("foo", "bar"); + ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); + when(coder.encode(eq(state), (ParseOperationSet) isNull(), any(PointerEncoder.class))) + .thenReturn(json); + FileObjectStore store = new FileObjectStore<>(ParseUser.class, file, coder); + + ParseUser user = mock(ParseUser.class); + when(user.getState()).thenReturn(state); + ParseTaskUtils.wait(store.setAsync(user)); + + JSONObject jsonAgain = ParseFileUtils.readFileToJSONObject(file); + assertEquals(json, jsonAgain, JSONCompareMode.STRICT); + } + + @Test + public void testGetAsync() throws Exception { + File file = new File(temporaryFolder.getRoot(), "test"); + + JSONObject json = new JSONObject(); + ParseFileUtils.writeJSONObjectToFile(file, json); + + ParseUser.State.Builder builder = new ParseUser.State.Builder(); + builder.put("foo", "bar"); + ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); + when(coder.decode(any(ParseUser.State.Builder.class), any(JSONObject.class), any(ParseDecoder.class))) + .thenReturn(builder); + FileObjectStore store = new FileObjectStore<>(ParseUser.class, file, coder); + + ParseUser user = ParseTaskUtils.wait(store.getAsync()); + assertEquals("bar", user.getState().get("foo")); + } + + @Test + public void testExistsAsync() throws Exception { + File file = temporaryFolder.newFile("test"); + FileObjectStore store = new FileObjectStore<>(ParseUser.class, file, null); + assertTrue(ParseTaskUtils.wait(store.existsAsync())); + + temporaryFolder.delete(); + assertFalse(ParseTaskUtils.wait(store.existsAsync())); + } + + @Test + public void testDeleteAsync() throws Exception { + File file = temporaryFolder.newFile("test"); + FileObjectStore store = new FileObjectStore<>(ParseUser.class, file, null); + assertTrue(file.exists()); + + ParseTaskUtils.wait(store.deleteAsync()); + assertFalse(file.exists()); + } +} \ No newline at end of file diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/InstallationIdTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/InstallationIdTest.java new file mode 100644 index 0000000..41a8e3c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/InstallationIdTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +public class InstallationIdTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testGetGeneratesInstallationIdAndFile() throws Exception{ + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + String installationIdString = installationId.get(); + assertNotNull(installationIdString); + assertEquals(installationIdString, + ParseFileUtils.readFileToString(installationIdFile, "UTF-8")); + } + + @Test + public void testGetReadsInstallationIdFromFile() throws Exception { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + ParseFileUtils.writeStringToFile(installationIdFile, "test_installation_id", "UTF-8"); + assertEquals("test_installation_id", installationId.get()); + } + + @Test + public void testSetWritesInstallationIdToFile() throws Exception { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + installationId.set("test_installation_id"); + assertEquals("test_installation_id", + ParseFileUtils.readFileToString(installationIdFile, "UTF-8")); + } + + @Test + public void testSetThenGet() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + installationId.set("test_installation_id"); + assertEquals("test_installation_id", installationId.get()); + } + + @Test + public void testInstallationIdIsCachedInMemory() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + String installationIdString = installationId.get(); + ParseFileUtils.deleteQuietly(installationIdFile); + assertEquals(installationIdString, installationId.get()); + } + + @Test + public void testInstallationIdIsRandom() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + + String installationIdString = new InstallationId(installationIdFile).get(); + ParseFileUtils.deleteQuietly(installationIdFile); + assertFalse(installationIdString.equals(new InstallationId(installationIdFile).get())); + } + + @Test + public void testSetSameDoesNotWriteToDisk() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + String installationIdString = installationId.get(); + ParseFileUtils.deleteQuietly(installationIdFile); + installationId.set(installationIdString); + assertFalse(installationIdFile.exists()); + } + + @Test + public void testSetNullDoesNotPersist() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + String installationIdString = installationId.get(); + installationId.set(null); + assertEquals(installationIdString, installationId.get()); + } + + @Test + public void testSetEmptyStringDoesNotPersist() { + File installationIdFile = new File(temporaryFolder.getRoot(), "installationId"); + InstallationId installationId = new InstallationId(installationIdFile); + + String installationIdString = installationId.get(); + installationId.set(""); + assertEquals(installationIdString, installationId.get()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ListsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ListsTest.java new file mode 100644 index 0000000..0ff6d9b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ListsTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ListsTest { + + @Test + public void testPartition() throws Exception { + List list = new ArrayList<>(); + for (int i = 0; i < 99; i++) { + list.add(i); + } + List> partitions = Lists.partition(list, 5); + assertEquals(20, partitions.size()); + + int count = 0; + for (int i = 0; i < 19; i++) { + List partition = partitions.get(i); + assertEquals(5, partition.size()); + for (int j : partition) { + assertEquals(count, j); + count += 1; + } + } + assertEquals(4, partitions.get(19).size()); + for (int i = 0; i < 4; i++) { + assertEquals(95 + i, partitions.get(19).get(i).intValue()); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/LocalIdManagerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/LocalIdManagerTest.java new file mode 100644 index 0000000..56b9e4f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/LocalIdManagerTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class LocalIdManagerTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testLocalIdManager() throws Exception { + LocalIdManager manager = new LocalIdManager(temporaryFolder.newFolder("test")); + manager.clear(); + + String localId1 = manager.createLocalId(); + assertNotNull(localId1); + manager.retainLocalIdOnDisk(localId1); // refcount = 1 + assertNull(manager.getObjectId(localId1)); + + String localId2 = manager.createLocalId(); + assertNotNull(localId2); + manager.retainLocalIdOnDisk(localId2); // refcount = 1 + assertNull(manager.getObjectId(localId2)); + + manager.retainLocalIdOnDisk(localId1); // refcount = 2 + assertNull(manager.getObjectId(localId1)); + assertNull(manager.getObjectId(localId2)); + + manager.releaseLocalIdOnDisk(localId1); // refcount = 1 + assertNull(manager.getObjectId(localId1)); + assertNull(manager.getObjectId(localId2)); + + String objectId1 = "objectId1"; + manager.setObjectId(localId1, objectId1); + assertEquals(objectId1, manager.getObjectId(localId1)); + assertNull(manager.getObjectId(localId2)); + + manager.retainLocalIdOnDisk(localId1); // refcount = 2 + assertEquals(objectId1, manager.getObjectId(localId1)); + assertNull(manager.getObjectId(localId2)); + + String objectId2 = "objectId2"; + manager.setObjectId(localId2, objectId2); + assertEquals(objectId1, manager.getObjectId(localId1)); + assertEquals(objectId2, manager.getObjectId(localId2)); + + manager.releaseLocalIdOnDisk(localId1); // refcount = 1 + assertEquals(objectId1, manager.getObjectId(localId1)); + assertEquals(objectId2, manager.getObjectId(localId2)); + + manager.releaseLocalIdOnDisk(localId1); // refcount = 0 + assertNull(manager.getObjectId(localId1)); + assertEquals(objectId2, manager.getObjectId(localId2)); + + manager.releaseLocalIdOnDisk(localId2); // refcount = 0 + assertNull(manager.getObjectId(localId1)); + assertNull(manager.getObjectId(localId2)); + + assertFalse(manager.clear()); + } + + @Test + public void testLongSerialization() throws Exception { + long expected = 0x8000000000000000L; + JSONObject object = new JSONObject(); + object.put("hugeNumber", expected); + String json = object.toString(); + + object = new JSONObject(json); + long actual = object.getLong("hugeNumber"); + assertEquals(expected, actual); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkObjectControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkObjectControllerTest.java new file mode 100644 index 0000000..a5df28f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkObjectControllerTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.ByteArrayInputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +// For Uri.encode +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class NetworkObjectControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testFetchAsync + + @Test + public void testFetchAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = new JSONObject(); + String createAtStr = "2015-08-09T22:15:13.460Z"; + long createAtLong = ParseDateFormat.getInstance().parse(createAtStr).getTime(); + String updateAtStr = "2015-08-09T22:15:13.497Z"; + long updateAtLong = ParseDateFormat.getInstance().parse(updateAtStr).getTime(); + mockResponse.put("createdAt", createAtStr); + mockResponse.put("objectId", "testObjectId"); + mockResponse.put("key", "value"); + mockResponse.put("updatedAt", updateAtStr); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make test state + ParseObject.State state = new ParseObject.State.Builder("Test") + .objectId("testObjectId") + .build(); + + NetworkObjectController controller = new NetworkObjectController(restClient); + ParseObject.State newState = + ParseTaskUtils.wait(controller.fetchAsync(state, "sessionToken", ParseDecoder.get())); + + assertEquals(createAtLong, newState.createdAt()); + assertEquals(updateAtLong, newState.updatedAt()); + assertEquals("value", newState.get("key")); + assertEquals("testObjectId", newState.objectId()); + assertTrue(newState.isComplete()); + } + + //endregion + + //region testSaveAsync + + @Test + public void testSaveAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = new JSONObject(); + String createAtStr = "2015-08-09T22:15:13.460Z"; + long createAtLong = ParseDateFormat.getInstance().parse(createAtStr).getTime(); + String updateAtStr = "2015-08-09T22:15:13.497Z"; + long updateAtLong = ParseDateFormat.getInstance().parse(updateAtStr).getTime(); + mockResponse.put("createdAt", createAtStr); + mockResponse.put("objectId", "testObjectId"); + mockResponse.put("updatedAt", updateAtStr); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make test state + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + + NetworkObjectController controller = new NetworkObjectController(restClient); + ParseObject.State newState = ParseTaskUtils.wait(controller.saveAsync( + object.getState(), + object.startSave(), + "sessionToken", + ParseDecoder.get())); + + assertEquals(createAtLong, newState.createdAt()); + assertEquals(updateAtLong, newState.updatedAt()); + assertEquals("testObjectId", newState.objectId()); + assertFalse(newState.isComplete()); + } + + //endregion + + //region testDeleteAsync + + @Test + public void testDeleteAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = new JSONObject(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make test state + ParseObject.State state = new ParseObject.State.Builder("Test") + .objectId("testObjectId") + .build(); + + NetworkObjectController controller = new NetworkObjectController(restClient); + // We just need to verify task is finished since sever returns an empty json here + ParseTaskUtils.wait(controller.deleteAsync(state, "sessionToken")); + } + + //endregion + + //region testSaveAllAsync + + @Test + public void testSaveAllAsync() throws Exception { + // Make individual responses + JSONObject objectSaveResult = new JSONObject(); + String createAtStr = "2015-08-09T22:15:13.460Z"; + long createAtLong = ParseDateFormat.getInstance().parse(createAtStr).getTime(); + String updateAtStr = "2015-08-09T22:15:13.497Z"; + long updateAtLong = ParseDateFormat.getInstance().parse(updateAtStr).getTime(); + objectSaveResult.put("createdAt", createAtStr); + objectSaveResult.put("objectId", "testObjectId"); + objectSaveResult.put("updatedAt", updateAtStr); + JSONObject objectResponse = new JSONObject(); + objectResponse.put("success", objectSaveResult); + JSONObject objectResponseAgain = new JSONObject(); + JSONObject objectSaveResultAgain = new JSONObject(); + objectSaveResultAgain.put("code", 101); + objectSaveResultAgain.put("error", "Error"); + objectResponseAgain.put("error", objectSaveResultAgain); + // Make batch response + JSONArray mockResponse = new JSONArray(); + mockResponse.put(objectResponse); + mockResponse.put(objectResponseAgain); + // Make mock response + byte[] contentBytes = mockResponse.toString().getBytes(); + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(contentBytes)) + .setStatusCode(200) + .setTotalSize(contentBytes.length) + .setContentType("application/json") + .build(); + // Mock http client + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + // Make test state, operations and decoder + List states = new ArrayList<>(); + List operationsList = new ArrayList<>(); + List decoders = new ArrayList<>(); + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + states.add(object.getState()); + operationsList.add(object.startSave()); + decoders.add(ParseDecoder.get()); + ParseObject objectAgain = new ParseObject("Test"); + object.put("keyAgain", "valueAgain"); + states.add(objectAgain.getState()); + operationsList.add(objectAgain.startSave()); + decoders.add(ParseDecoder.get()); + + // Test + NetworkObjectController controller = new NetworkObjectController(client); + List> saveTaskList = + controller.saveAllAsync(states, operationsList, "sessionToken", decoders); + Task.whenAll(saveTaskList).waitForCompletion(); + + // Verify newState + ParseObject.State newState = saveTaskList.get(0).getResult(); + assertEquals(createAtLong, newState.createdAt()); + assertEquals(updateAtLong, newState.updatedAt()); + assertEquals("testObjectId", newState.objectId()); + assertFalse(newState.isComplete()); + // Verify exception + assertTrue(saveTaskList.get(1).isFaulted()); + assertTrue(saveTaskList.get(1).getError() instanceof ParseException); + ParseException parseException = (ParseException) saveTaskList.get(1).getError(); + assertEquals(101, parseException.getCode()); + assertEquals("Error", parseException.getMessage()); + } + + //endregion + + //region testDeleteAsync + + @Test + public void testDeleteAllAsync() throws Exception { + // Make individual responses + JSONObject objectResponse = new JSONObject(); + objectResponse.put("success", new JSONObject()); + JSONObject objectResponseAgain = new JSONObject(); + JSONObject objectDeleteResultAgain = new JSONObject(); + objectDeleteResultAgain.put("code", 101); + objectDeleteResultAgain.put("error", "Error"); + objectResponseAgain.put("error", objectDeleteResultAgain); + // Make batch response + JSONArray mockResponse = new JSONArray(); + mockResponse.put(objectResponse); + mockResponse.put(objectResponseAgain); + // Make mock response + byte[] contentBytes = mockResponse.toString().getBytes(); + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(contentBytes)) + .setStatusCode(200) + .setTotalSize(contentBytes.length) + .setContentType("application/json") + .build(); + // Mock http client + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + // Make test state, operations and decoder + List states = new ArrayList<>(); + // Make test state + ParseObject.State state = new ParseObject.State.Builder("Test") + .objectId("testObjectId") + .build(); + states.add(state); + ParseObject.State stateAgain = new ParseObject.State.Builder("Test") + .objectId("testObjectIdAgain") + .build(); + states.add(stateAgain); + + // Test + NetworkObjectController controller = new NetworkObjectController(client); + List> deleteTaskList = + controller.deleteAllAsync(states, "sessionToken"); + Task.whenAll(deleteTaskList).waitForCompletion(); + + // Verify success result + assertFalse(deleteTaskList.get(0).isFaulted()); + // Verify error result + assertTrue(deleteTaskList.get(1).isFaulted()); + assertTrue(deleteTaskList.get(1).getError() instanceof ParseException); + ParseException parseException = (ParseException) deleteTaskList.get(1).getError(); + assertEquals(101, parseException.getCode()); + assertEquals("Error", parseException.getMessage()); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkQueryControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkQueryControllerTest.java new file mode 100644 index 0000000..846a65c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkQueryControllerTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NetworkQueryControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testConvertFindResponse + + @Test + public void testConvertFindResponse() throws Exception { + // Make mock response + JSONObject mockResponse = generateBasicMockResponse(); + // Make mock state + ParseQuery.State mockState = mock(ParseQuery.State.class); + when(mockState.className()).thenReturn("Test"); + when(mockState.selectedKeys()).thenReturn(null); + when(mockState.constraints()).thenReturn(new ParseQuery.QueryConstraints()); + + NetworkQueryController controller = new NetworkQueryController(mock(ParseHttpClient.class)); + List objects = controller.convertFindResponse(mockState, mockResponse); + + verifyBasicParseObjects(mockResponse, objects, "Test"); + } + + //endregion + + //region testFindAsync + + @Test + public void testFindAsyncWithSessionToken() throws Exception { + // Make mock response + JSONObject mockResponse = generateBasicMockResponse(); + mockResponse.put("trace", "serverTrace"); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make mock state + ParseQuery.State mockState = mock(ParseQuery.State.class); + when(mockState.className()).thenReturn("Test"); + when(mockState.selectedKeys()).thenReturn(null); + when(mockState.constraints()).thenReturn(new ParseQuery.QueryConstraints()); + + NetworkQueryController controller = new NetworkQueryController(restClient); + Task> findTask = controller.findAsync(mockState, "sessionToken", null); + ParseTaskUtils.wait(findTask); + List objects = findTask.getResult(); + + verifyBasicParseObjects(mockResponse, objects, "Test"); + // TODO(mengyan): Verify PLog is called + } + + // TODO(mengyan): Add testFindAsyncWithCachePolicy to verify command is added to + // ParseKeyValueCache + + //endregion + + //region testCountAsync + + @Test + public void testCountAsyncWithSessionToken() throws Exception { + // Make mock response and client + JSONObject mockResponse = new JSONObject(); + mockResponse.put("count", 2); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make mock state + ParseQuery.State mockState = mock(ParseQuery.State.class); + when(mockState.className()).thenReturn("Test"); + when(mockState.selectedKeys()).thenReturn(null); + when(mockState.constraints()).thenReturn(new ParseQuery.QueryConstraints()); + + NetworkQueryController controller = new NetworkQueryController(restClient); + + Task countTask = controller.countAsync(mockState, "sessionToken", null); + ParseTaskUtils.wait(countTask); + int count = countTask.getResult(); + + assertEquals(2, count); + } + + // TODO(mengyan): Add testFindAsyncWithCachePolicy to verify command is added to + // ParseKeyValueCache + + //endregion + + private static JSONObject generateBasicMockResponse() throws JSONException { + JSONObject objectJSON = new JSONObject(); + String createAtStr = "2015-08-09T22:15:13.460Z"; + objectJSON.put("createdAt", createAtStr); + objectJSON.put("updatedAt", createAtStr); + objectJSON.put("objectId", "testObjectId"); + objectJSON.put("sessionToken", "testSessionToken"); + objectJSON.put("key", "value"); + + createAtStr = "2015-08-10T22:15:13.460Z"; + JSONObject objectJSONAgain = new JSONObject(); + objectJSONAgain.put("createdAt", createAtStr); + objectJSONAgain.put("updatedAt", createAtStr); + objectJSONAgain.put("objectId", "testObjectIdAgain"); + objectJSONAgain.put("sessionToken", "testSessionTokenAgain"); + objectJSONAgain.put("keyAgain", "valueAgain"); + + JSONArray objectJSONArray = new JSONArray(); + objectJSONArray.put(objectJSON); + objectJSONArray.put(objectJSONAgain); + + JSONObject mockResponse = new JSONObject(); + mockResponse.put("results", objectJSONArray); + return mockResponse; + } + + private void verifyBasicParseObjects( + JSONObject mockResponse, List objects, String className) throws JSONException { + JSONArray objectsJSON = mockResponse.getJSONArray("results"); + assertEquals(objectsJSON.length(), objects.size()); + + ParseObject object = objects.get(0); + JSONObject objectJSON = objectsJSON.getJSONObject(0); + assertEquals(className, object.getClassName()); + long dateLong = + ParseDateFormat.getInstance().parse(objectJSON.getString("createdAt")).getTime(); + assertEquals(dateLong, object.getState().createdAt()); + dateLong = ParseDateFormat.getInstance().parse(objectJSON.getString("updatedAt")).getTime(); + assertEquals(dateLong, object.getState().updatedAt()); + assertEquals(objectJSON.getString("objectId"), object.getObjectId()); + assertEquals(objectJSON.getString("sessionToken"), object.get("sessionToken")); + assertEquals(objectJSON.getString("key"), object.getString("key")); + + ParseObject objectAgain = objects.get(1); + assertEquals(className, objectAgain.getClassName()); + JSONObject objectAgainJSON = objectsJSON.getJSONObject(1); + dateLong = + ParseDateFormat.getInstance().parse(objectAgainJSON.getString("createdAt")).getTime(); + assertEquals(dateLong, objectAgain.getState().createdAt()); + dateLong = + ParseDateFormat.getInstance().parse(objectAgainJSON.getString("updatedAt")).getTime(); + assertEquals(dateLong, objectAgain.getState().updatedAt()); + assertEquals(objectAgainJSON.getString("objectId"), objectAgain.getObjectId()); + assertEquals(objectAgainJSON.getString("sessionToken"), objectAgain.get("sessionToken")); + assertEquals(objectAgainJSON.getString("keyAgain"), objectAgain.getString("keyAgain")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkSessionControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkSessionControllerTest.java new file mode 100644 index 0000000..b4f2141 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkSessionControllerTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +// For Uri.encode +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class NetworkSessionControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testGetSessionAsync + + @Test + public void testGetSessionAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + mockResponse.put("installationId", "39c8e8a4-6dd0-4c39-ac85-7fd61425083b"); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + + NetworkSessionController controller = new NetworkSessionController(restClient); + ParseObject.State newState = + ParseTaskUtils.wait(controller.getSessionAsync("sessionToken")); + + // Verify session state + verifyBasicSessionState(mockResponse, newState); + assertEquals("39c8e8a4-6dd0-4c39-ac85-7fd61425083b", newState.get("installationId")); + + assertTrue(newState.isComplete()); + + } + + //endregion + + //region testUpgradeToRevocable + + @Test + public void testUpgradeToRevocable() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + + NetworkSessionController controller = new NetworkSessionController(restClient); + ParseObject.State newState = + ParseTaskUtils.wait(controller.upgradeToRevocable("sessionToken")); + + // Verify session state + verifyBasicSessionState(mockResponse, newState); + + assertTrue(newState.isComplete()); + + } + + //endregion + + //region testRevokeAsync + + @Test + public void testRevokeAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = new JSONObject(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + + NetworkSessionController controller = new NetworkSessionController(restClient); + // We just need to verify task is finished since sever returns an empty json here + ParseTaskUtils.wait(controller.revokeAsync("sessionToken")); + } + + //endregion + + private static JSONObject generateBasicMockResponse() throws JSONException { + JSONObject mockResponse = new JSONObject(); + mockResponse.put("createdAt", "2015-08-09T22:15:13.460Z"); + mockResponse.put("objectId", "testObjectId"); + mockResponse.put("sessionToken", "r:aBnrECraOBEXJSNMdtQJW36Re"); + mockResponse.put("restricted", "false"); + JSONObject createWith = new JSONObject(); + createWith.put("action", "upgrade"); + mockResponse.put("createdWith", createWith); + return mockResponse; + } + + private static void verifyBasicSessionState(JSONObject mockResponse, ParseSession.State state) + throws JSONException { + assertEquals("_Session", state.className()); + long createAtLong = + ParseDateFormat.getInstance().parse(mockResponse.getString("createdAt")).getTime(); + assertEquals(createAtLong, state.createdAt()); + assertEquals(mockResponse.getString("objectId"), state.objectId()); + assertEquals(mockResponse.getString("sessionToken"), state.get("sessionToken")); + assertEquals(mockResponse.getString("restricted"), state.get("restricted")); + assertEquals( + mockResponse.getJSONObject("createdWith").getString("action"), + ((Map)state.get("createdWith")).get("action")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkUserControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkUserControllerTest.java new file mode 100644 index 0000000..dc56231 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/NetworkUserControllerTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +// For Uri.encode +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class NetworkUserControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testSignUpAsync + + @Test + public void testSignUpAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(generateBasicMockResponse(), 200, "OK"); + // Make test user state and operationSet + ParseUser.State state = new ParseUser.State.Builder() + .put("username", "testUserName") + .put("password", "testPassword") + .build(); + ParseOperationSet operationSet = new ParseOperationSet(); + operationSet.put("username", new ParseSetOperation("testUserName")); + operationSet.put("password", new ParseSetOperation("testPassword")); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait( + controller.signUpAsync(state, operationSet, "sessionToken")); + + verifyBasicUserState(mockResponse, newState); + assertFalse(newState.isComplete()); + assertTrue(newState.isNew()); + } + + //endregion + + //region testLoginAsync + + @Test + public void testLoginAsyncWithUserNameAndPassword() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + + NetworkUserController controller = new NetworkUserController(restClient); + ParseUser.State newState = ParseTaskUtils.wait(controller.logInAsync("userName", "password")); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertTrue(newState.isComplete()); + assertFalse(newState.isNew()); + } + + @Test + public void testLoginAsyncWithUserStateCreated() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 201, "OK"); + // Make test user state and operationSet + ParseUser.State state = new ParseUser.State.Builder() + .put("username", "testUserName") + .put("password", "testPassword") + .build(); + ParseOperationSet operationSet = new ParseOperationSet(); + operationSet.put("username", new ParseSetOperation("testUserName")); + operationSet.put("password", new ParseSetOperation("testPassword")); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait(controller.logInAsync(state, operationSet)); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertTrue(newState.isNew()); + assertFalse(newState.isComplete()); + } + + @Test + public void testLoginAsyncWithUserStateNotCreated() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make test user state and operationSet + ParseUser.State state = new ParseUser.State.Builder() + .put("username", "testUserName") + .put("password", "testPassword") + .build(); + ParseOperationSet operationSet = new ParseOperationSet(); + operationSet.put("username", new ParseSetOperation("testUserName")); + operationSet.put("password", new ParseSetOperation("testPassword")); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait(controller.logInAsync(state, operationSet)); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertFalse(newState.isNew()); + assertTrue(newState.isComplete()); + } + + @Test + public void testLoginAsyncWithAuthTypeCreated() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + mockResponse.put("authData", generateMockAuthData()); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 201, "OK"); + // Make test user auth data + Map facebookAuthInfoMap = new HashMap<>(); + facebookAuthInfoMap.put("token", "test"); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait( + controller.logInAsync("facebook", facebookAuthInfoMap)); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertTrue(newState.isNew()); + assertTrue(newState.isComplete()); + } + + @Test + public void testLoginAsyncWithAuthTypeNotCreated() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + mockResponse.put("authData", generateMockAuthData()); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + // Make test user auth data + Map facebookAuthInfoMap = new HashMap<>(); + facebookAuthInfoMap.put("token", "test"); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait( + controller.logInAsync("facebook", facebookAuthInfoMap)); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertFalse(newState.isNew()); + assertTrue(newState.isComplete()); + } + + //endregion + + //region testGetUserAsync + + @Test + public void testGetUserAsync() throws Exception { + // Make mock response and client + JSONObject mockResponse = generateBasicMockResponse(); + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 201, "OK"); + + NetworkUserController controller = new NetworkUserController(restClient, true); + ParseUser.State newState = ParseTaskUtils.wait(controller.getUserAsync("sessionToken")); + + // Verify user state + verifyBasicUserState(mockResponse, newState); + + assertTrue(newState.isComplete()); + assertFalse(newState.isNew()); + } + + //endregion + + //region testRequestPasswordResetAsync + + @Test + public void testRequestPasswordResetAsync() throws Exception { + // Make mock response and client + ParseHttpClient restClient = + ParseTestUtils.mockParseHttpClientWithResponse(new JSONObject(), 200, "OK"); + + NetworkUserController controller = new NetworkUserController(restClient, true); + // We just need to verify task is finished since sever returns an empty json here + ParseTaskUtils.wait(controller.requestPasswordResetAsync("sessionToken")); + } + + //endregion + + private JSONObject generateBasicMockResponse() throws JSONException { + JSONObject mockResponse = new JSONObject(); + String createAtStr = "2015-08-09T22:15:13.460Z"; + mockResponse.put("createdAt", createAtStr); + mockResponse.put("objectId", "testObjectId"); + mockResponse.put("sessionToken", "testSessionToken"); + mockResponse.put("username", "testUserName"); + mockResponse.put("email", "test@parse.com"); + return mockResponse; + } + + private JSONObject generateMockAuthData() throws JSONException { + JSONObject facebookAuthInfo = new JSONObject(); + facebookAuthInfo.put("token", "test"); + JSONObject facebookAuthData = new JSONObject(); + facebookAuthData.put("facebook", facebookAuthInfo); + return facebookAuthData; + } + + private void verifyBasicUserState(JSONObject mockResponse, ParseUser.State state) + throws JSONException { + long createAtLong = + ParseDateFormat.getInstance().parse(mockResponse.getString("createdAt")).getTime(); + assertEquals(createAtLong, state.createdAt()); + assertEquals(mockResponse.getString("objectId"), state.objectId()); + assertEquals(mockResponse.getString("sessionToken"), state.sessionToken()); + assertEquals(mockResponse.getString("username"), state.get("username")); + assertEquals(mockResponse.getString("email"), state.get("email")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineObjectStoreTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineObjectStoreTest.java new file mode 100644 index 0000000..9e88159 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineObjectStoreTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import java.util.Arrays; +import java.util.Collections; + +import bolts.Task; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyList; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OfflineObjectStoreTest { + + private static final String PIN_NAME = "test"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseUser.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseUser.class); + Parse.setLocalDatastore(null); + ParseCorePlugins.getInstance().reset(); + } + + @Test + public void testSetAsync() throws Exception { + OfflineStore lds = mock(OfflineStore.class); + when(lds.unpinAllObjectsAsync(anyString())).thenReturn(Task.forResult(null)); + when(lds.pinAllObjectsAsync(anyString(), anyList(), anyBoolean())) + .thenReturn(Task.forResult(null)); + Parse.setLocalDatastore(lds); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, null); + + ParseUser user = mock(ParseUser.class); + ParseTaskUtils.wait(store.setAsync(user)); + + verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); + verify(user, times(1)).pinInBackground(PIN_NAME, false); + } + + //region getAsync + + @Test + public void testGetAsyncFromLDS() throws Exception { + Parse.enableLocalDatastore(null); + ParseUser user = mock(ParseUser.class); + ParseQueryController queryController = mock(ParseQueryController.class); + //noinspection unchecked + when(queryController.findAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class)) + ).thenReturn(Task.forResult(Collections.singletonList(user))); + ParseCorePlugins.getInstance().registerQueryController(queryController); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, null); + + ParseUser userAgain = ParseTaskUtils.wait(store.getAsync()); + //noinspection unchecked + verify(queryController, times(1)) + .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + assertSame(user, userAgain); + } + + @Test + public void testGetAsyncFromLDSWithTooManyObjects() throws Exception { + Parse.enableLocalDatastore(null); + ParseQueryController queryController = mock(ParseQueryController.class); + //noinspection unchecked + when(queryController.findAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class)) + ).thenReturn(Task.forResult(Arrays.asList(mock(ParseUser.class), mock(ParseUser.class)))); + ParseCorePlugins.getInstance().registerQueryController(queryController); + OfflineStore lds = mock(OfflineStore.class); + when(lds.unpinAllObjectsAsync(anyString())).thenReturn(Task.forResult(null)); + Parse.setLocalDatastore(lds); + + @SuppressWarnings("unchecked") + ParseObjectStore legacy = mock(ParseObjectStore.class); + when(legacy.getAsync()).thenReturn(Task.forResult(null)); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); + + ParseUser user = ParseTaskUtils.wait(store.getAsync()); + //noinspection unchecked + verify(queryController, times(1)) + .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); + assertNull(user); + } + + @Test + public void testGetAsyncMigrate() throws Exception { + Parse.enableLocalDatastore(null); + ParseQueryController queryController = mock(ParseQueryController.class); + //noinspection unchecked + when(queryController.findAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class)) + ).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerQueryController(queryController); + OfflineStore lds = mock(OfflineStore.class); + when(lds.pinAllObjectsAsync(anyString(), anyList(), anyBoolean())) + .thenReturn(Task.forResult(null)); + when(lds.unpinAllObjectsAsync(anyString())).thenReturn(Task.forResult(null)); + when(lds.pinAllObjectsAsync(anyString(), anyList(), anyBoolean())) + .thenReturn(Task.forResult(null)); + Parse.setLocalDatastore(lds); + + ParseUser user = mock(ParseUser.class); + when(user.pinInBackground(anyString(), anyBoolean())).thenReturn(Task.forResult(null)); + @SuppressWarnings("unchecked") + ParseObjectStore legacy = mock(ParseObjectStore.class); + when(legacy.getAsync()).thenReturn(Task.forResult(user)); + when(legacy.deleteAsync()).thenReturn(Task.forResult(null)); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); + + ParseUser userAgain = ParseTaskUtils.wait(store.getAsync()); + //noinspection unchecked + verify(queryController, times(1)) + .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + verify(legacy, times(1)).getAsync(); + verify(legacy, times(1)).deleteAsync(); + verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); + verify(user, times(1)).pinInBackground(PIN_NAME, false); + assertNotNull(userAgain); + assertSame(user, userAgain); + } + + //endregion + + //region existsAsync + + @Test + public void testExistsAsyncLDS() throws Exception { + Parse.enableLocalDatastore(null); + ParseQueryController queryController = mock(ParseQueryController.class); + //noinspection unchecked + when(queryController.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class)) + ).thenReturn(Task.forResult(1)); + ParseCorePlugins.getInstance().registerQueryController(queryController); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, null); + assertTrue(ParseTaskUtils.wait(store.existsAsync())); + //noinspection unchecked + verify(queryController, times(1)).countAsync( + any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + } + + @Test + public void testExistsAsyncLegacy() throws Exception { + Parse.enableLocalDatastore(null); + ParseQueryController queryController = mock(ParseQueryController.class); + //noinspection unchecked + when(queryController.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class)) + ).thenReturn(Task.forResult(0)); + ParseCorePlugins.getInstance().registerQueryController(queryController); + + @SuppressWarnings("unchecked") + ParseObjectStore legacy = mock(ParseObjectStore.class); + when(legacy.existsAsync()).thenReturn(Task.forResult(true)); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); + + assertTrue(ParseTaskUtils.wait(store.existsAsync())); + //noinspection unchecked + verify(queryController, times(1)).countAsync( + any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + } + + //endregion + + //region deleteAsync + + @Test + public void testDeleteAsync() throws Exception { + OfflineStore lds = mock(OfflineStore.class); + when(lds.unpinAllObjectsAsync(anyString())).thenReturn(Task.forResult(null)); + Parse.setLocalDatastore(lds); + + @SuppressWarnings("unchecked") + ParseObjectStore legacy = mock(ParseObjectStore.class); + when(legacy.deleteAsync()).thenReturn(Task.forResult(null)); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); + + ParseTaskUtils.wait(store.deleteAsync()); + verify(legacy, times(1)).deleteAsync(); + verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); + } + + @Test + public void testDeleteAsyncFailure() throws Exception { + OfflineStore lds = mock(OfflineStore.class); + when(lds.unpinAllObjectsAsync(anyString())) + .thenReturn(Task.forError(new RuntimeException("failure"))); + Parse.setLocalDatastore(lds); + + @SuppressWarnings("unchecked") + ParseObjectStore legacy = mock(ParseObjectStore.class); + when(legacy.deleteAsync()).thenReturn(Task.forResult(null)); + + OfflineObjectStore store = + new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); + + thrown.expect(RuntimeException.class); + ParseTaskUtils.wait(store.deleteAsync()); + verify(legacy, times(1)).deleteAsync(); + } + + //endregion +} + diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryControllerTest.java new file mode 100644 index 0000000..3f82a25 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryControllerTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import bolts.Task; + +import static org.junit.Assert.assertTrue; + +public class OfflineQueryControllerTest { + + @After + public void tearDown() { + Parse.disableLocalDatastore(); + } + + @Test + public void testFindFromNetwork() { + TestNetworkQueryController networkController = new TestNetworkQueryController(); + OfflineQueryController controller = new OfflineQueryController(null, networkController); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .build(); + controller.findAsync(state, null, null); + networkController.verifyFind(); + } + + @Test + public void testCountFromNetwork() { + TestNetworkQueryController networkController = new TestNetworkQueryController(); + OfflineQueryController controller = new OfflineQueryController(null, networkController); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .build(); + controller.countAsync(state, null, null); + networkController.verifyCount(); + } + + @Test + public void testGetFromNetwork() { + TestNetworkQueryController networkController = new TestNetworkQueryController(); + OfflineQueryController controller = new OfflineQueryController(null, networkController); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .build(); + controller.getFirstAsync(state, null, null); + networkController.verifyFind(); + } + + @Test + public void testFindFromLDS() { + Parse.enableLocalDatastore(null); + TestOfflineStore offlineStore = new TestOfflineStore(); + OfflineQueryController controller = new OfflineQueryController(offlineStore, null); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .fromLocalDatastore() + .build(); + controller.findAsync(state, null, null); + offlineStore.verifyFind(); + } + + @Test + public void testCountFromLDS() { + Parse.enableLocalDatastore(null); + TestOfflineStore offlineStore = new TestOfflineStore(); + OfflineQueryController controller = new OfflineQueryController(offlineStore, null); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .fromLocalDatastore() + .build(); + controller.countAsync(state, null, null); + offlineStore.verifyCount(); + } + + @Test + public void testGetFromLDS() { + Parse.enableLocalDatastore(null); + TestOfflineStore offlineStore = new TestOfflineStore(); + OfflineQueryController controller = new OfflineQueryController(offlineStore, null); + + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .fromLocalDatastore() + .build(); + controller.getFirstAsync(state, null, null); + offlineStore.verifyFind(); + } + + private static class TestOfflineStore extends OfflineStore { + + private AtomicBoolean findCalled = new AtomicBoolean(); + private AtomicBoolean countCalled = new AtomicBoolean(); + + TestOfflineStore() { + super((OfflineSQLiteOpenHelper) null); + } + + @Override + Task> findFromPinAsync( + String name, ParseQuery.State state, ParseUser user) { + findCalled.set(true); + return Task.forResult(null); + } + + @Override + Task countFromPinAsync( + String name, ParseQuery.State state, ParseUser user) { + countCalled.set(true); + return Task.forResult(null); + } + + public void verifyFind() { + assertTrue(findCalled.get()); + } + + public void verifyCount() { + assertTrue(countCalled.get()); + } + } + + private static class TestNetworkQueryController implements ParseQueryController { + + private AtomicBoolean findCalled = new AtomicBoolean(); + private AtomicBoolean countCalled = new AtomicBoolean(); + + @Override + public Task> findAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + findCalled.set(true); + return Task.forResult(null); + } + + @Override + public Task countAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + countCalled.set(true); + return Task.forResult(null); + } + + @Override + public Task getFirstAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + throw new IllegalStateException("Should not be called"); + } + + public void verifyFind() { + assertTrue(findCalled.get()); + } + + public void verifyCount() { + assertTrue(countCalled.get()); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryLogicTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryLogicTest.java new file mode 100644 index 0000000..f7d84c0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/OfflineQueryLogicTest.java @@ -0,0 +1,1178 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import bolts.Task; + +import static com.parse.ParseMatchers.hasParseErrorCode; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for OfflineQueryLogic. + */ +public class OfflineQueryLogicTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @After + public void tearDown() { + ParseCorePlugins.getInstance().reset(); + } + + //region hasReadAccess + + @Test + public void testHasReadAccessWithSameObject() { + ParseUser user = mock(ParseUser.class); + + assertTrue(OfflineQueryLogic.hasReadAccess(user, user)); + verify(user, never()).getACL(); + } + + @Test + public void testHasReadAccessWithNoACL() { + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(null); + + assertTrue(OfflineQueryLogic.hasReadAccess(null, object)); + } + + @Test + public void testHasReadAccessWithPublicReadAccess() { + ParseACL acl = mock(ParseACL.class); + when(acl.getPublicReadAccess()).thenReturn(true); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertTrue(OfflineQueryLogic.hasReadAccess(null, object)); + } + + @Test + public void testHasReadAccessWithReadAccess() { + ParseUser user = mock(ParseUser.class); + when(user.getObjectId()).thenReturn("test"); + + ParseACL acl = mock(ParseACL.class); + when(acl.getReadAccess(user)).thenReturn(true); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertTrue(OfflineQueryLogic.hasReadAccess(user, object)); + } + + @Test + public void testHasReadAccessWithNoReadAccess() { + ParseACL acl = mock(ParseACL.class); + when(acl.getPublicReadAccess()).thenReturn(false); + when(acl.getReadAccess(any(ParseUser.class))).thenReturn(false); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertFalse(OfflineQueryLogic.hasReadAccess(null, object)); + } + + //endregion + + //region hasWriteAccess + + @Test + public void testHasWriteAccessWithSameObject() { + ParseUser user = mock(ParseUser.class); + + assertTrue(OfflineQueryLogic.hasWriteAccess(user, user)); + verify(user, never()).getACL(); + } + + @Test + public void testHasWriteAccessWithNoACL() { + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(null); + + assertTrue(OfflineQueryLogic.hasWriteAccess(null, object)); + } + + @Test + public void testHasWriteAccessWithPublicWriteAccess() { + ParseACL acl = mock(ParseACL.class); + when(acl.getPublicWriteAccess()).thenReturn(true); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertTrue(OfflineQueryLogic.hasWriteAccess(null, object)); + } + + @Test + public void testHasWriteAccessWithWriteAccess() { + ParseUser user = mock(ParseUser.class); + + ParseACL acl = mock(ParseACL.class); + when(acl.getWriteAccess(user)).thenReturn(true); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertTrue(OfflineQueryLogic.hasWriteAccess(user, object)); + } + + @Test + public void testHasWriteAccessWithNoWriteAccess() { + ParseACL acl = mock(ParseACL.class); + when(acl.getPublicReadAccess()).thenReturn(false); + + ParseObject object = mock(ParseObject.class); + when(object.getACL()).thenReturn(acl); + + assertFalse(OfflineQueryLogic.hasWriteAccess(null, object)); + } + + //endregion + + //region createMatcher + + @Test + public void testMatcherWithNoReadAccess() throws ParseException { + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .build(); + + ParseACL acl = new ParseACL(); + acl.setPublicReadAccess(false); + ParseObject object = new ParseObject("TestObject"); + object.setACL(acl); + + ParseUser user = mock(ParseUser.class); + when(user.getObjectId()).thenReturn("test"); + + assertFalse(matches(logic, query, object, user)); + } + + @Test + public void testSimpleMatcher() throws ParseException { + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + ParseObject objectA = new ParseObject("TestObject"); + objectA.put("value", "A"); + objectA.put("foo", "bar"); + + ParseObject objectB = new ParseObject("TestObject"); + objectB.put("value", "B"); + objectB.put("foo", "bar"); + + ParseQuery.State queryA = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", "A") + .whereEqualTo("foo", "bar") + .build(); + + assertTrue(matches(logic, queryA, objectA)); + assertFalse(matches(logic, queryA, objectB)); + } + + @Test + public void testOrMatcher() throws Exception { + ParseObject objectA = new ParseObject("TestObject"); + objectA.put("value", "A"); + + ParseObject objectB = new ParseObject("TestObject"); + objectB.put("value", "B"); + + ParseQuery.State query = ParseQuery.State.Builder.or(Arrays.asList( + new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", "A"), + new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", "B") + )).build(); + + OfflineQueryLogic logic = new OfflineQueryLogic(null); + assertTrue(matches(logic, query, objectA)); + assertTrue(matches(logic, query, objectB)); + } + + @Test + public void testAndMatcher() throws Exception { + ParseObject objectA = new ParseObject("TestObject"); + objectA.put("foo", "bar"); + + ParseObject objectB = new ParseObject("TestObject"); + objectB.put("baz", "qux"); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("foo", "$ne", "bar") + .addCondition("baz", "$ne", "qux") + .build(); + + OfflineQueryLogic logic = new OfflineQueryLogic(null); + assertFalse(matches(logic, query, objectA)); + assertFalse(matches(logic, query, objectB)); + } + + // TODO(grantland): testRelationMatcher() + + //endregion + + //region matchesEquals + + @Test + public void testMatchesEqualsWithGeoPoint() throws Exception { + ParseGeoPoint point = new ParseGeoPoint(37.774929f, -122.419416f); // SF + ParseObject object = new ParseObject("TestObject"); + object.put("point", point); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("point", point) + .build(); + assertTrue(matches(logic, query, object)); + + // Test lat + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("point", new ParseGeoPoint(37.774929f, -74.005941f)) + .build(); + assertFalse(matches(logic, query, object)); + + // Test lng + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("point", new ParseGeoPoint(40.712784f, -122.419416f)) + .build(); + assertFalse(matches(logic, query, object)); + + // Not GeoPoint + object = new ParseObject("TestObject"); + object.put("point", "A"); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesEqualsWithPolygon() throws Exception { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParsePolygon polygon = new ParsePolygon(points); + ParseObject object = new ParseObject("TestObject"); + object.put("polygon", polygon); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("polygon", polygon) + .build(); + assertTrue(matches(logic, query, object)); + + List diff = new ArrayList(); + diff.add(new ParseGeoPoint(0,0)); + diff.add(new ParseGeoPoint(0,10)); + diff.add(new ParseGeoPoint(10,10)); + diff.add(new ParseGeoPoint(10,0)); + diff.add(new ParseGeoPoint(0,0)); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("polygon", new ParsePolygon(diff)) + .build(); + assertFalse(matches(logic, query, object)); + + // Not Polygon + object = new ParseObject("TestObject"); + object.put("polygon", "A"); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesEqualsWithNumbers() throws ParseException { + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + ParseQuery.State iQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", /* (int) */ 5) + .build(); + + ParseObject iObject = new ParseObject("TestObject"); + iObject.put("value", /* (int) */ 5); + assertTrue(matches(logic, iQuery, iObject)); + + ParseObject object = new ParseObject("TestObject"); + object.put("value", "string"); + assertFalse(matches(logic, iQuery, object)); + + ParseObject noMatch = new ParseObject("TestObject"); + noMatch.put("value", 6); + assertFalse(matches(logic, iQuery, noMatch)); + } + + @Test + public void testMatchesEqualsNull() throws ParseException { + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + ParseObject object = new ParseObject("TestObject"); + object.put("value", "test"); + + ParseObject nullObject = new ParseObject("TestObject"); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", "test") + .build(); + + ParseQuery.State nullQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", null) + .build(); + + assertTrue(matches(logic, query, object)); + assertFalse(matches(logic, query, nullObject)); + + assertFalse(matches(logic, nullQuery, object)); + assertTrue(matches(logic, nullQuery, nullObject)); + } + + //endregion + + @Test + public void testMatchesIn() throws ParseException { + ParseObject object = new ParseObject("TestObject"); + object.put("foo", "bar"); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("foo", "$in", Arrays.asList("bar", "baz")) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("foo", "$in", Collections.singletonList("qux")) + .build(); + assertFalse(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + object.put("foo", JSONObject.NULL); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesAll() throws Exception { + ParseObject object = new ParseObject("TestObject"); + object.put("foo", Arrays.asList("foo", "bar")); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("foo", "$all", Arrays.asList("foo", "bar")) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("foo", "$all", Arrays.asList("foo", "bar", "qux")) + .build(); + assertFalse(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + object.put("foo", JSONObject.NULL); + assertFalse(matches(logic, query, object)); + + thrown.expect(IllegalArgumentException.class); + object.put("foo", "bar"); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesNearSphere() throws Exception { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + ParseGeoPoint sf = new ParseGeoPoint(37.774929f, -122.419416f); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", fb); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereNear("point", fb) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereNear("point", sf) + .maxDistance("point", 0.00628) + .build(); + assertFalse(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereNear("point", sf) + .maxDistance("point", 0.00629) + .build(); + assertTrue(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + } + + //region matchesWithin + + @Test + public void testMatchesWithinFailureInternationalDateLine() throws ParseException { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + ParseGeoPoint sf = new ParseGeoPoint(37.774929f, -122.419416f); + ParseGeoPoint sj = new ParseGeoPoint(37.338208f, -121.886329f); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", fb); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + thrown.expect(ParseException.class); + thrown.expect(hasParseErrorCode(ParseException.INVALID_QUERY)); + thrown.expectMessage("whereWithinGeoBox queries cannot cross the International Date Line."); + query = new ParseQuery.State.Builder<>("TestObject") + .whereWithin("point", sj, sf) + .build(); + matches(logic, query, object); + } + + @Test + public void testMatchesWithinFailureSwapped() throws Exception { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + ParseGeoPoint sf = new ParseGeoPoint(37.774929f, -122.419416f); + ParseGeoPoint sj = new ParseGeoPoint(37.338208f, -121.886329f); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", fb); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + thrown.expect(ParseException.class); + thrown.expect(hasParseErrorCode(ParseException.INVALID_QUERY)); + thrown.expectMessage( + "The southwest corner of a geo box must be south of the northeast corner."); + query = new ParseQuery.State.Builder<>("TestObject") + .whereWithin("point", sf, sj) + .build(); + matches(logic, query, object); + } + + @Test + public void testMatchesWithinFailure180() throws Exception { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + ParseGeoPoint sf = new ParseGeoPoint(37.774929f, -122.419416f); + ParseGeoPoint beijing = new ParseGeoPoint(39.904211f, 116.407395f); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", fb); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + thrown.expect(ParseException.class); + thrown.expect(hasParseErrorCode(ParseException.INVALID_QUERY)); + thrown.expectMessage("Geo box queries larger than 180 degrees in longitude are not supported. " + + "Please check point order."); + query = new ParseQuery.State.Builder<>("TestObject") + .whereWithin("point", sf, beijing) + .build(); + matches(logic, query, object); + } + + @Test + public void testMatchesWithin() throws ParseException { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + ParseGeoPoint sunset = new ParseGeoPoint(37.746731f, -122.486349f); + ParseGeoPoint soma = new ParseGeoPoint(37.778519f, -122.40564f); + ParseGeoPoint twinPeaks = new ParseGeoPoint(37.754407f, -122.447684f); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", fb); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + object.put("point", twinPeaks); + query = new ParseQuery.State.Builder<>("TestObject") + .whereWithin("point", sunset, soma) + .build(); + assertTrue(matches(logic, query, object)); + + object.put("point", fb); + assertFalse(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesGeoIntersects() throws ParseException { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParseGeoPoint inside = new ParseGeoPoint(0.5,0.5); + ParseGeoPoint outside = new ParseGeoPoint(10,10); + + ParsePolygon polygon = new ParsePolygon(points); + + ParseObject object = new ParseObject("TestObject"); + object.put("polygon", polygon); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + query = new ParseQuery.State.Builder<>("TestObject") + .whereGeoIntersects("polygon", inside) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereGeoIntersects("polygon", outside) + .build(); + assertFalse(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testMatchesGeoWithin() throws ParseException { + List smallBox = new ArrayList(); + smallBox.add(new ParseGeoPoint(0,0)); + smallBox.add(new ParseGeoPoint(0,1)); + smallBox.add(new ParseGeoPoint(1,1)); + smallBox.add(new ParseGeoPoint(1,0)); + + List largeBox = new ArrayList(); + largeBox.add(new ParseGeoPoint(0,0)); + largeBox.add(new ParseGeoPoint(0,10)); + largeBox.add(new ParseGeoPoint(10,10)); + largeBox.add(new ParseGeoPoint(10,0)); + + ParseGeoPoint point = new ParseGeoPoint(5,5); + + //ParsePolygon polygon = new ParsePolygon(points); + + ParseObject object = new ParseObject("TestObject"); + object.put("point", point); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + query = new ParseQuery.State.Builder<>("TestObject") + .whereGeoWithin("point", largeBox) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereGeoWithin("point", smallBox) + .build(); + assertFalse(matches(logic, query, object)); + + // Non-existant key + object = new ParseObject("TestObject"); + assertFalse(matches(logic, query, object)); + } + + //endregion + + //region compare + + @Test + public void testCompareList() throws Exception { + ParseObject object = new ParseObject("SomeObject"); + List list = new ArrayList<>(); + list.add(1); + list.add(2); + list.add(3); + object.put("list", list); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("SomeObject") + .whereEqualTo("list", 2) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("SomeObject") + .whereEqualTo("list", 4) + .build(); + assertFalse(matches(logic, query, object)); + } + + @Test + public void testCompareJSONArray() throws Exception { + ParseObject object = new ParseObject("SomeObject"); + JSONArray array = new JSONArray(); + array.put(1); + array.put(2); + array.put(3); + object.put("array", array); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("SomeObject") + .whereEqualTo("array", 2) + .build(); + assertTrue(matches(logic, query, object)); + + query = new ParseQuery.State.Builder<>("SomeObject") + .whereEqualTo("array", 4) + .build(); + assertFalse(matches(logic, query, object)); + } + + //endregion + + //region compareTo + + @Test + public void testCompareToNumber() throws Exception { + ParseObject object = new ParseObject("TestObject"); + object.put("value", 5); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", /* (int) */ 5) + .build(); + assertTrue(matches(logic, query, object)); + object.put("value", 6); + assertFalse(matches(logic, query, object)); + object.put("value", 5); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("value", "$lt", 6) + .build(); + assertTrue(matches(logic, query, object)); + object.put("value", 6); + assertFalse(matches(logic, query, object)); + object.put("value", 5); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("value", "$gt", 4) + .build(); + assertTrue(matches(logic, query, object)); + object.put("value", 4); + assertFalse(matches(logic, query, object)); + object.put("value", 5); + + // TODO(grantland): Move below to NumbersTest + + ParseObject iObject = new ParseObject("TestObject"); + iObject.put("value", /* (int) */ 5); + + ParseObject dObject = new ParseObject("TestObject"); + dObject.put("value", /* (double) */ 5.0); + + ParseObject fObject = new ParseObject("TestObject"); + fObject.put("value", /* (float) */ 5.0f); + + ParseObject lObject = new ParseObject("TestObject"); + lObject.put("value", (long) 5); + + ParseQuery.State iQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", /* (int) */ 5) + .build(); + + ParseQuery.State dQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", /* (double) */ 5.0) + .build(); + + ParseQuery.State fQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", /* (float) */ 5.0f) + .build(); + + ParseQuery.State lQuery = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("value", (long) 5) + .build(); + + assertTrue(matches(logic, iQuery, iObject)); + assertTrue(matches(logic, iQuery, dObject)); + assertTrue(matches(logic, iQuery, fObject)); + assertTrue(matches(logic, iQuery, lObject)); + + assertTrue(matches(logic, dQuery, iObject)); + assertTrue(matches(logic, dQuery, dObject)); + assertTrue(matches(logic, dQuery, fObject)); + assertTrue(matches(logic, dQuery, lObject)); + + assertTrue(matches(logic, fQuery, iObject)); + assertTrue(matches(logic, fQuery, dObject)); + assertTrue(matches(logic, fQuery, fObject)); + assertTrue(matches(logic, fQuery, lObject)); + + assertTrue(matches(logic, lQuery, iObject)); + assertTrue(matches(logic, lQuery, dObject)); + assertTrue(matches(logic, lQuery, fObject)); + assertTrue(matches(logic, lQuery, lObject)); + } + + @Test + public void testCompareToDate() throws Exception { + Date date = new Date(); + Date before = new Date(date.getTime() - 20); + Date after = new Date(date.getTime() + 20); + + ParseObject object = new ParseObject("TestObject"); + object.put("date", date); + + ParseQuery.State query; + OfflineQueryLogic logic = new OfflineQueryLogic(null); + + query = new ParseQuery.State.Builder<>("TestObject") + .whereEqualTo("date", date) + .build(); + assertTrue(matches(logic, query, object)); + object.put("date", after); + assertFalse(matches(logic, query, object)); + object.put("date", date); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("date", "$lt", after) + .build(); + assertTrue(matches(logic, query, object)); + object.put("date", after); + assertFalse(matches(logic, query, object)); + object.put("date", date); + + query = new ParseQuery.State.Builder<>("TestObject") + .addCondition("date", "$gt", before) + .build(); + assertTrue(matches(logic, query, object)); + object.put("date", before); + assertFalse(matches(logic, query, object)); + object.put("date", after); + } + + //endregion + + //region Sort + + @Test + public void testSortInvalidKey() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addAscendingOrder("_test") + .build(); + + thrown.expect(ParseException.class); + OfflineQueryLogic.sort(null, query); + } + + @Test + public void testSortWithNoOrder() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .build(); + + OfflineQueryLogic.sort(null, query); + } + + @Test + public void testSortWithGeoQuery() throws ParseException { + ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .whereNear("point", fb) + .build(); + + List objects = new ArrayList<>(); + ParseObject object; + + object = new ParseObject("TestObject"); + object.put("name", "sf"); + object.put("point", new ParseGeoPoint(37.774929f, -122.419416f)); + objects.add(object); + + object = new ParseObject("TestObject"); + object.put("name", "ny"); + object.put("point", new ParseGeoPoint(40.712784f, -74.005941f)); + objects.add(object); + + object = new ParseObject("TestObject"); + object.put("name", "mpk"); + object.put("point", new ParseGeoPoint(37.452960f, -122.181725f)); + objects.add(object); + + OfflineQueryLogic.sort(objects, query); + assertEquals("mpk", objects.get(0).getString("name")); + assertEquals("sf", objects.get(1).getString("name")); + assertEquals("ny", objects.get(2).getString("name")); + } + + @Test + public void testSortDescending() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addDescendingOrder("name") + .build(); + + List objects = new ArrayList<>(); + ParseObject object; + + object = new ParseObject("TestObject"); + object.put("name", "grantland"); + objects.add(object); + object = new ParseObject("TestObject"); + object.put("name", "nikita"); + objects.add(object); + object = new ParseObject("TestObject"); + object.put("name", "listiarso"); + objects.add(object); + + OfflineQueryLogic.sort(objects, query); + assertEquals("nikita", objects.get(0).getString("name")); + assertEquals("listiarso", objects.get(1).getString("name")); + assertEquals("grantland", objects.get(2).getString("name")); + } + + @Test + public void testQuerySortNumber() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addAscendingOrder("key") + .build(); + + List results = generateParseObjects("key", new Object[]{ + /* (int) */ 8, + /* (double) */ 7.0, + /* (float) */ 6.0f, + (long) 5 + }); + + OfflineQueryLogic.sort(results, query); + + int last = 0; + for (ParseObject result : results) { + int current = result.getInt("key"); + assertTrue(current > last); + last = current; + } + } + + @Test + public void testQuerySortNull() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addAscendingOrder("key") + .build(); + + List results = generateParseObjects("key", new Object[]{ + null, + "value", + null + }); + + OfflineQueryLogic.sort(results, query); + + assertEquals(0, results.get(0).getInt("id")); + assertEquals(2, results.get(1).getInt("id")); + assertEquals(1, results.get(2).getInt("id")); + } + + @Test + public void testQuerySortDifferentTypes() throws ParseException { + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .addAscendingOrder("key") + .build(); + + List results = generateParseObjects("key", new Object[]{ + "string", + 5 + }); + + thrown.expect(IllegalArgumentException.class); + OfflineQueryLogic.sort(results, query); + } + + //endregion + + //region fetchIncludes + + @Test + public void testFetchIncludesParseObject() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseSQLiteDatabase db = mock(ParseSQLiteDatabase.class); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = new ParseObject("TestObject"); + ParseObject unfetchedObject = new ParseObject("TestObject"); + object.put("foo", unfetchedObject); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, db)); + verify(store).fetchLocallyAsync(object, db); + verify(store).fetchLocallyAsync(unfetchedObject, db); + verifyNoMoreInteractions(store); + } + + @Test + public void testFetchIncludesCollection() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseSQLiteDatabase db = mock(ParseSQLiteDatabase.class); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = mock(ParseObject.class); + ParseObject unfetchedObject = mock(ParseObject.class); + Collection objects = new ArrayList<>(); + objects.add(unfetchedObject); + when(object.get("foo")).thenReturn(objects); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, db)); + verify(store).fetchLocallyAsync(object, db); + verify(store).fetchLocallyAsync(unfetchedObject, db); + verifyNoMoreInteractions(store); + } + + @Test + public void testFetchIncludesJSONArray() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseSQLiteDatabase db = mock(ParseSQLiteDatabase.class); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = mock(ParseObject.class); + ParseObject unfetchedObject = mock(ParseObject.class); + JSONArray objects = new JSONArray(); + objects.put(unfetchedObject); + when(object.get("foo")).thenReturn(objects); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, db)); + verify(store).fetchLocallyAsync(object, db); + verify(store).fetchLocallyAsync(unfetchedObject, db); + verifyNoMoreInteractions(store); + } + + @Test + public void testFetchIncludesMap() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseSQLiteDatabase db = mock(ParseSQLiteDatabase.class); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo.bar") + .build(); + + ParseObject object = mock(ParseObject.class); + ParseObject unfetchedObject = mock(ParseObject.class); + Map objects = new HashMap<>(); + objects.put("bar", unfetchedObject); + when(object.get("foo")).thenReturn(objects); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, db)); + verify(store).fetchLocallyAsync(object, db); + verify(store).fetchLocallyAsync(unfetchedObject, db); + verifyNoMoreInteractions(store); + } + + @Test + public void testFetchIncludesJSONObject() throws Exception { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseSQLiteDatabase db = mock(ParseSQLiteDatabase.class); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo.bar") + .build(); + + ParseObject object = mock(ParseObject.class); + ParseObject unfetchedObject = mock(ParseObject.class); + JSONObject objects = new JSONObject(); + objects.put("bar", unfetchedObject); + when(object.get("foo")).thenReturn(objects); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, db)); + verify(store).fetchLocallyAsync(object, db); + verify(store).fetchLocallyAsync(unfetchedObject, db); + verifyNoMoreInteractions(store); + } + + @Test + public void testFetchIncludesNull() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = new ParseObject("TestObject"); + object.put("foo", JSONObject.NULL); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); + // only itself + verify(store, times(1)) + .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + } + + @Test + public void testFetchIncludesNonParseObject() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = new ParseObject("TestObject"); + object.put("foo", 1); + + thrown.expect(ParseException.class); + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); + // only itself + verify(store, times(1)) + .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + } + + @Test + public void testFetchIncludesDoesNotExist() throws ParseException { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo") + .build(); + + ParseObject object = new ParseObject("TestObject"); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); + // only itself + verify(store, times(1)) + .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + } + + @Test + public void testFetchIncludesNestedNull() throws Exception { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo.bar") + .build(); + + ParseObject object = new ParseObject("TestObject"); + object.put("foo", JSONObject.NULL); + + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); + // only itself + verify(store, times(1)) + .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + } + + @Test + public void testFetchIncludesNestedNonParseObject() throws Exception { + OfflineStore store = mock(OfflineStore.class); + when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + .thenReturn(Task.forResult(null)); + + ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") + .include("foo.bar") + .build(); + + ParseObject object = new ParseObject("TestObject"); + object.put("foo", 1); + + thrown.expect(IllegalStateException.class); + ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); + // only itself + verify(store, times(1)) + .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + } + + //endregion + + private static boolean matches( + OfflineQueryLogic logic, ParseQuery.State query, T object) throws ParseException { + return matches(logic, query, object, null); + } + + private static boolean matches( + OfflineQueryLogic logic, ParseQuery.State query, T object, ParseUser user) + throws ParseException { + Task task = logic.createMatcher(query, user).matchesAsync(object, null); + return ParseTaskUtils.wait(task); + } + + private static List generateParseObjects( + String key, Object[] values) { + List objects = new ArrayList<>(); + int i = 0; + for (Object value : values) { + ParseObject object = ParseObject.create("TestObject"); + object.put("id", i); + if (value != null) { + object.put(key, value); + } + objects.add(object); + + i++; + } + return objects; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseACLTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseACLTest.java new file mode 100644 index 0000000..b34787a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseACLTest.java @@ -0,0 +1,690 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseACLTest { + + private final static String UNRESOLVED_KEY = "*unresolved"; + private static final String READ_PERMISSION = "read"; + private static final String WRITE_PERMISSION = "write"; + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseRole.class); + ParseObject.registerSubclass(ParseUser.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseRole.class); + ParseObject.unregisterSubclass(ParseUser.class); + } + + //region testConstructor + + @Test + public void testConstructor() throws Exception { + ParseACL acl = new ParseACL(); + + assertEquals(0, acl.getPermissionsById().size()); + } + + @Test + public void testConstructorWithUser() throws Exception { + ParseUser user = new ParseUser(); + user.setObjectId("test"); + ParseACL acl = new ParseACL(user); + + assertTrue(acl.getReadAccess("test")); + assertTrue(acl.getWriteAccess("test")); + } + + //endregion + + //region testCopy + + @Test + public void testCopy() throws Exception { + ParseACL acl = new ParseACL(); + final ParseUser unresolvedUser = mock(ParseUser.class); + when(unresolvedUser.isLazy()).thenReturn(true); + // This will set unresolvedUser and permissionsById + acl.setReadAccess(unresolvedUser, true); + acl.setWriteAccess(unresolvedUser, true); + // We need to reset unresolvedUser since registerSaveListener will be triggered once in + // setReadAccess() + reset(unresolvedUser); + + ParseACL copiedACL = new ParseACL(acl); + + assertEquals(1, copiedACL.getPermissionsById().size()); + assertTrue(copiedACL.getPermissionsById().containsKey(UNRESOLVED_KEY)); + assertTrue(copiedACL.getReadAccess(unresolvedUser)); + assertTrue(copiedACL.getWriteAccess(unresolvedUser)); + assertFalse(copiedACL.isShared()); + assertSame(unresolvedUser, copiedACL.getUnresolvedUser()); + verify(unresolvedUser, times(1)).registerSaveListener(any(GetCallback.class)); + } + + @Test + public void testCopyWithSaveListener() throws Exception { + ParseACL acl = new ParseACL(); + final ParseUser unresolvedUser = mock(ParseUser.class); + when(unresolvedUser.isLazy()).thenReturn(true); + // This will set unresolvedUser and permissionsById + acl.setReadAccess(unresolvedUser, true); + acl.setWriteAccess(unresolvedUser, true); + // We need to reset unresolvedUser since registerSaveListener will be triggered once in + // setReadAccess() + reset(unresolvedUser); + + ParseACL copiedACL = new ParseACL(acl); + + // Make sure the callback is called + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(GetCallback.class); + verify(unresolvedUser, times(1)).registerSaveListener(callbackCaptor.capture()); + + // Trigger the callback + GetCallback callback = callbackCaptor.getValue(); + // Manually set userId and not lazy, mock user is saved + when(unresolvedUser.getObjectId()).thenReturn("userId"); + when(unresolvedUser.isLazy()).thenReturn(false); + callback.done(unresolvedUser, null); + + // Makre sure we unregister the callback + verify(unresolvedUser, times(1)).unregisterSaveListener(any(GetCallback.class)); + assertEquals(1, copiedACL.getPermissionsById().size()); + assertTrue(copiedACL.getReadAccess(unresolvedUser)); + assertTrue(copiedACL.getWriteAccess(unresolvedUser)); + assertFalse(copiedACL.isShared()); + // No more unresolved permissions since it has been resolved in the callback. + assertFalse(copiedACL.getPermissionsById().containsKey(UNRESOLVED_KEY)); + assertNull(copiedACL.getUnresolvedUser()); + } + + //endregion + + //region toJson + + @Test + public void testToJson() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + ParseUser unresolvedUser = new ParseUser(); + setLazy(unresolvedUser); + acl.setReadAccess(unresolvedUser, true); + // Mock decoder + ParseEncoder mockEncoder = mock(ParseEncoder.class); + when(mockEncoder.encode(eq(unresolvedUser))).thenReturn("unresolvedUserJson"); + + JSONObject aclJson = acl.toJSONObject(mockEncoder); + + assertEquals("unresolvedUserJson", aclJson.getString("unresolvedUser")); + assertEquals(aclJson.getJSONObject("userId").getBoolean("read"), true); + assertEquals(aclJson.getJSONObject("userId").has("write"), false); + assertEquals(aclJson.getJSONObject("*unresolved").getBoolean("read"), true); + assertEquals(aclJson.getJSONObject("*unresolved").has("write"), false); + assertEquals(aclJson.length(), 3); + } + + //endregion + + //region parcelable + + @Test + public void testParcelable() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + ParseUser user = new ParseUser(); + user.setObjectId("userId2"); + acl.setReadAccess(user, true); + acl.setRoleWriteAccess("role", true); + acl.setShared(true); + + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + acl = ParseACL.CREATOR.createFromParcel(parcel); + + assertTrue(acl.getReadAccess("userId")); + assertTrue(acl.getReadAccess(user)); + assertTrue(acl.getRoleWriteAccess("role")); + assertTrue(acl.isShared()); + assertFalse(acl.getPublicReadAccess()); + assertFalse(acl.getPublicWriteAccess()); + } + + @Test + public void testParcelableWithUnresolvedUser() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); // Needed for unparceling ParseObjects + ParseACL acl = new ParseACL(); + ParseUser unresolved = new ParseUser(); + setLazy(unresolved); + acl.setReadAccess(unresolved, true); + + // unresolved users need a local id when parcelling and unparcelling. + // Since we don't have an Android environment, local id creation will fail. + unresolved.localId = "localId"; + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + // Do not user ParseObjectParcelDecoder because it requires local ids + acl = new ParseACL(parcel, new ParseParcelDecoder()); + assertTrue(acl.getReadAccess(unresolved)); + } + + //endregion + + //region testCreateACLFromJSONObject + + @Test + public void testCreateACLFromJSONObject() throws Exception { + JSONObject aclJson = new JSONObject(); + JSONObject permission = new JSONObject(); + permission.put(READ_PERMISSION, true); + permission.put(WRITE_PERMISSION, true); + aclJson.put("userId", permission); + ParseUser unresolvedUser = new ParseUser(); + JSONObject unresolvedUserJson = new JSONObject(); + aclJson.put("unresolvedUser", unresolvedUserJson); + // Mock decoder + ParseDecoder mockDecoder = mock(ParseDecoder.class); + when(mockDecoder.decode(eq(unresolvedUserJson))).thenReturn(unresolvedUser); + + ParseACL acl = ParseACL.createACLFromJSONObject(aclJson, mockDecoder); + + assertSame(unresolvedUser, acl.getUnresolvedUser()); + assertTrue(acl.getReadAccess("userId")); + assertTrue(acl.getWriteAccess("userId")); + assertEquals(1, acl.getPermissionsById().size()); + } + + //endregion + + //region testResolveUser + + @Test + public void testResolveUserWithNewUser() throws Exception { + ParseUser unresolvedUser = new ParseUser(); + setLazy(unresolvedUser); + ParseACL acl = new ParseACL(); + acl.setReadAccess(unresolvedUser, true); + + ParseUser other = new ParseUser(); + // local id creation fails if we don't have Android environment + unresolvedUser.localId = "someId"; + other.localId = "someOtherId"; + acl.resolveUser(other); + + // Make sure unresolvedUser is not changed + assertSame(unresolvedUser, acl.getUnresolvedUser()); + } + + @Test + public void testResolveUserWithUnresolvedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser unresolvedUser = new ParseUser(); + setLazy(unresolvedUser); + // This will set the unresolvedUser in acl + acl.setReadAccess(unresolvedUser, true); + acl.setWriteAccess(unresolvedUser, true); + + unresolvedUser.setObjectId("test"); + acl.resolveUser(unresolvedUser); + + assertNull(acl.getUnresolvedUser()); + assertTrue(acl.getReadAccess(unresolvedUser)); + assertTrue(acl.getWriteAccess(unresolvedUser)); + assertEquals(1, acl.getPermissionsById().size()); + assertFalse(acl.getPermissionsById().containsKey(UNRESOLVED_KEY)); + } + + //endregion + + //region testSetAccess + + @Test + public void testSetAccessWithNoPermissionAndNotAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setReadAccess("userId", false); + + // Make sure noting is set + assertEquals(0, acl.getPermissionsById().size()); + } + + @Test + public void testSetAccessWithAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setReadAccess("userId", true); + + assertTrue(acl.getReadAccess("userId")); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test + public void testSetAccessWithPermissionsAndNotAllowed() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + + acl.setReadAccess("userId", false); + + // Make sure we remove the read access + assertFalse(acl.getReadAccess("userId")); + assertEquals(0, acl.getPermissionsById().size()); + } + + @Test + public void testSetPublicReadAccessAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setPublicReadAccess(true); + + assertTrue(acl.getPublicReadAccess()); + } + + @Test + public void testSetPublicReadAccessNotAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setPublicReadAccess(false); + + // Make sure noting is set + assertEquals(0, acl.getPermissionsById().size()); + } + + @Test + public void testSetPublicWriteAccessAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setPublicWriteAccess(true); + + assertTrue(acl.getPublicWriteAccess()); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test + public void testSetPublicWriteAccessNotAllowed() throws Exception { + ParseACL acl = new ParseACL(); + + acl.setPublicWriteAccess(false); + + // Make sure noting is set + assertEquals(0, acl.getPermissionsById().size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetReadAccessWithNullUserId() throws Exception { + ParseACL acl = new ParseACL(); + + String userId = null; + acl.setReadAccess(userId, true); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetWriteAccessWithNullUserId() throws Exception { + ParseACL acl = new ParseACL(); + + String userId = null; + acl.setWriteAccess(userId, true); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetRoleReadAccessWithInvalidRole() throws Exception { + ParseRole role = new ParseRole(); + role.setName("Player"); + ParseACL acl = new ParseACL(); + + acl.setRoleReadAccess(role, true); + } + + @Test + public void testSetRoleReadAccess() throws Exception { + ParseRole role = new ParseRole(); + role.setName("Player"); + role.setObjectId("test"); + ParseACL acl = new ParseACL(); + + acl.setRoleReadAccess(role, true); + + assertTrue(acl.getRoleReadAccess(role)); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetRoleWriteAccessWithInvalidRole() throws Exception { + ParseRole role = new ParseRole(); + role.setName("Player"); + ParseACL acl = new ParseACL(); + + acl.setRoleWriteAccess(role, true); + } + + @Test + public void testSetRoleWriteAccess() throws Exception { + ParseRole role = new ParseRole(); + role.setName("Player"); + role.setObjectId("test"); + ParseACL acl = new ParseACL(); + + acl.setRoleWriteAccess(role, true); + + assertTrue(acl.getRoleWriteAccess(role)); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetUserReadAccessWithNotSavedNotLazyUser() throws Exception { + ParseUser user = new ParseUser(); + ParseACL acl = new ParseACL(); + + acl.setReadAccess(user, true); + } + + @Test + public void testSetUserReadAccessWithLazyUser() throws Exception { + ParseUser unresolvedUser = mock(ParseUser.class); + when(unresolvedUser.isLazy()).thenReturn(true); + ParseACL acl = new ParseACL(); + + acl.setReadAccess(unresolvedUser, true); + + assertSame(unresolvedUser, acl.getUnresolvedUser()); + verify(unresolvedUser, times(1)).registerSaveListener(any(GetCallback.class)); + assertTrue(acl.getPermissionsById().containsKey(UNRESOLVED_KEY)); + assertTrue(acl.getReadAccess(unresolvedUser)); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test + public void testSetUserReadAccessWithNormalUser() throws Exception { + ParseUser user = new ParseUser(); + user.setObjectId("test"); + ParseACL acl = new ParseACL(); + + acl.setReadAccess(user, true); + + assertTrue(acl.getReadAccess(user)); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetUserWriteAccessWithNotSavedNotLazyUser() throws Exception { + ParseUser user = new ParseUser(); + ParseACL acl = new ParseACL(); + + acl.setWriteAccess(user, true); + } + + @Test + public void testSetUserWriteAccessWithLazyUser() throws Exception { + ParseUser user = mock(ParseUser.class); + when(user.isLazy()).thenReturn(true); + ParseACL acl = new ParseACL(); + + acl.setWriteAccess(user, true); + + assertSame(user, acl.getUnresolvedUser()); + verify(user, times(1)).registerSaveListener(any(GetCallback.class)); + assertTrue(acl.getPermissionsById().containsKey(UNRESOLVED_KEY)); + assertTrue(acl.getWriteAccess(user)); + assertEquals(1, acl.getPermissionsById().size()); + } + + @Test + public void testSetUserWriteAccessWithNormalUser() throws Exception { + ParseUser user = new ParseUser(); + user.setObjectId("test"); + ParseACL acl = new ParseACL(); + + acl.setWriteAccess(user, true); + + assertTrue(acl.getWriteAccess(user)); + assertEquals(1, acl.getPermissionsById().size()); + } + //endregion + + //region testGetAccess + + @Test + public void testGetAccessWithNoPermission() throws Exception { + ParseACL acl = new ParseACL(); + + assertFalse(acl.getReadAccess("userId")); + } + + @Test + public void testGetAccessWithNoAccessType() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + + assertFalse(acl.getWriteAccess("userId")); + } + + @Test + public void testGetAccessWithPermission() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + + assertTrue(acl.getReadAccess("userId")); + } + + @Test + public void testGetPublicReadAccess() throws Exception { + ParseACL acl = new ParseACL(); + acl.setPublicWriteAccess(true); + + assertTrue(acl.getPublicWriteAccess()); + } + + @Test + public void testGetPublicWriteAccess() throws Exception { + ParseACL acl = new ParseACL(); + acl.setPublicWriteAccess(true); + + assertTrue(acl.getPublicWriteAccess()); + } + + + @Test(expected = IllegalArgumentException.class) + public void testGetReadAccessWithNullUserId() throws Exception { + ParseACL acl = new ParseACL(); + + String userId = null; + acl.getReadAccess(userId); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetWriteAccessWithNullUserId() throws Exception { + ParseACL acl = new ParseACL(); + + String userId = null; + acl.getWriteAccess(userId); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetRoleReadAccessWithInvalidRole() throws Exception { + ParseACL acl = new ParseACL(); + ParseRole role = new ParseRole(); + role.setName("Player"); + + acl.getRoleReadAccess(role); + } + + @Test + public void testGetRoleReadAccess() throws Exception { + ParseACL acl = new ParseACL(); + ParseRole role = new ParseRole(); + role.setName("Player"); + role.setObjectId("test"); + acl.setRoleReadAccess(role, true); + + assertTrue(acl.getRoleReadAccess(role)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetRoleWriteAccessWithInvalidRole() throws Exception { + ParseACL acl = new ParseACL(); + ParseRole role = new ParseRole(); + role.setName("Player"); + + acl.getRoleWriteAccess(role); + } + + @Test + public void testGetRoleWriteAccess() throws Exception { + ParseACL acl = new ParseACL(); + ParseRole role = new ParseRole(); + role.setName("Player"); + role.setObjectId("test"); + acl.setRoleWriteAccess(role, true); + + assertTrue(acl.getRoleWriteAccess(role)); + } + + @Test + public void testGetUserReadAccessWithUnresolvedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + setLazy(user); + // Since user is a lazy user, this will set the acl's unresolved user and give it read access + acl.setReadAccess(user ,true); + + assertTrue(acl.getReadAccess(user)); + } + + @Test + public void testGetUserReadAccessWithLazyUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + setLazy(user); + + assertFalse(acl.getReadAccess(user)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetUserReadAccessWithNotSavedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + + assertFalse(acl.getReadAccess(user)); + } + + @Test + public void testGetUserReadAccessWithNormalUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + user.setObjectId("test"); + acl.setReadAccess(user, true); + + assertTrue(acl.getReadAccess(user)); + } + + @Test + public void testGetUserWriteAccessWithUnresolvedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + setLazy(user); + // Since user is a lazy user, this will set the acl's unresolved user and give it write access + acl.setWriteAccess(user, true); + + assertTrue(acl.getWriteAccess(user)); + } + + @Test + public void testGetUserWriteAccessWithLazyUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = mock(ParseUser.class); + when(user.isLazy()).thenReturn(true); + + assertFalse(acl.getWriteAccess(user)); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetUserWriteAccessWithNotSavedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + + assertFalse(acl.getWriteAccess(user)); + } + + @Test + public void testGetUserWriteAccessWithNormalUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + user.setObjectId("test"); + acl.setWriteAccess(user, true); + + assertTrue(acl.getWriteAccess(user)); + } + + //endregion + + //region testGetter/Setter + + @Test + public void testIsShared() throws Exception { + ParseACL acl = new ParseACL(); + acl.setShared(true); + + assertTrue(acl.isShared()); + } + + @Test + public void testUnresolvedUser() throws Exception { + ParseACL acl = new ParseACL(); + ParseUser user = new ParseUser(); + setLazy(user); + // This will set unresolvedUser in acl + acl.setReadAccess(user, true); + + assertTrue(acl.hasUnresolvedUser()); + assertSame(user, acl.getUnresolvedUser()); + } + + //endregion + + private static void setLazy(ParseUser user) { + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("anonymousToken", "anonymousTest"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java new file mode 100644 index 0000000..a5c0ee1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// For android.net.Uri +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseAnalyticsControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testConstructor + + @Test + public void testConstructor() { + ParseEventuallyQueue queue = mock(ParseEventuallyQueue.class); + ParseAnalyticsController controller = new ParseAnalyticsController(queue); + assertSame(queue, controller.eventuallyQueue); + } + + //endregion + + //region trackEventInBackground + + @Test + public void testTrackEvent() throws Exception { + // Mock eventually queue + ParseEventuallyQueue queue = mock(ParseEventuallyQueue.class); + when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), any(ParseObject.class))) + .thenReturn(Task.forResult(new JSONObject())); + + // Execute + ParseAnalyticsController controller = new ParseAnalyticsController(queue); + Map dimensions = new HashMap<>(); + dimensions.put("event", "close"); + ParseTaskUtils.wait(controller.trackEventInBackground("name", dimensions, "sessionToken")); + + // Verify eventuallyQueue.enqueueEventuallyAsync + ArgumentCaptor command = ArgumentCaptor.forClass(ParseRESTCommand.class); + ArgumentCaptor object = ArgumentCaptor.forClass(ParseObject.class); + verify(queue, times(1)).enqueueEventuallyAsync(command.capture(), object.capture()); + + // Verify eventuallyQueue.enqueueEventuallyAsync object parameter + assertNull(object.getValue()); + + // Verify eventuallyQueue.enqueueEventuallyAsync command parameter + assertTrue(command.getValue() instanceof ParseRESTAnalyticsCommand); + assertTrue(command.getValue().httpPath.contains("name")); + assertEquals("sessionToken", command.getValue().getSessionToken()); + JSONObject jsonDimensions = command.getValue().jsonParameters.getJSONObject("dimensions"); + assertEquals("close", jsonDimensions.get("event")); + assertEquals(1, jsonDimensions.length()); + } + + //endregion + + //region trackAppOpenedInBackground + + @Test + public void testTrackAppOpened() throws Exception { + // Mock eventually queue + ParseEventuallyQueue queue = mock(ParseEventuallyQueue.class); + when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), any(ParseObject.class))) + .thenReturn(Task.forResult(new JSONObject())); + + // Execute + ParseAnalyticsController controller = new ParseAnalyticsController(queue); + ParseTaskUtils.wait(controller.trackAppOpenedInBackground("pushHash", "sessionToken")); + + // Verify eventuallyQueue.enqueueEventuallyAsync + ArgumentCaptor command = ArgumentCaptor.forClass(ParseRESTCommand.class); + ArgumentCaptor object = ArgumentCaptor.forClass(ParseObject.class); + verify(queue, times(1)).enqueueEventuallyAsync(command.capture(), object.capture()); + + // Verify eventuallyQueue.enqueueEventuallyAsync object parameter + assertNull(object.getValue()); + + + // Verify eventuallyQueue.enqueueEventuallyAsync command parameter + assertTrue(command.getValue() instanceof ParseRESTAnalyticsCommand); + assertTrue(command.getValue().httpPath.contains(ParseRESTAnalyticsCommand.EVENT_APP_OPENED)); + assertEquals("sessionToken", command.getValue().getSessionToken()); + assertEquals("pushHash", command.getValue().jsonParameters.get("push_hash")); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsTest.java new file mode 100644 index 0000000..8e9106c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAnalyticsTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Intent; +import android.os.Bundle; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// For android.os.BaseBundle +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseAnalyticsTest { + + ParseAnalyticsController controller; + + @Before + public void setUp() { + ParseTestUtils.setTestParseUser(); + + // Mock ParseAnalyticsController + controller = mock(ParseAnalyticsController.class); + when(controller.trackEventInBackground( + anyString(), + anyMapOf(String.class, String.class), + anyString())).thenReturn(Task.forResult(null)); + when(controller.trackAppOpenedInBackground( + anyString(), + anyString())).thenReturn(Task.forResult(null)); + + ParseCorePlugins.getInstance().registerAnalyticsController(controller); + } + + @After + public void tearDown() { + ParseCorePlugins.getInstance().reset(); + // Clear LRU cache in ParseAnalytics + ParseAnalytics.clear(); + controller = null; + } + + // No need to test ParseAnalytics since it has no instance fields and all methods are static. + + @Test + public void testGetAnalyticsController() throws Exception{ + assertSame(controller, ParseAnalytics.getAnalyticsController()); + } + + //region trackEventInBackground + + @Test(expected = IllegalArgumentException.class) + public void testTrackEventInBackgroundNullName() throws Exception { + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground(null)); + } + + @Test(expected = IllegalArgumentException.class) + public void testTrackEventInBackgroundEmptyName() throws Exception{ + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground("")); + } + + @Test + public void testTrackEventInBackgroundNormalName() throws Exception{ + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground("test")); + + verify(controller, times(1)).trackEventInBackground( + eq("test"), Matchers.>eq(null), isNull(String.class)); + } + + @Test + public void testTrackEventInBackgroundNullParameters() throws Exception{ + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground("test", (Map) null)); + + verify(controller, times(1)).trackEventInBackground( + eq("test"), Matchers.>eq(null), isNull(String.class)); + } + + @Test + public void testTrackEventInBackgroundEmptyParameters() throws Exception{ + Map dimensions = new HashMap<>(); + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground("test", dimensions)); + + verify(controller, times(1)).trackEventInBackground( + eq("test"), eq(dimensions), isNull(String.class)); + } + + @Test + public void testTrackEventInBackgroundNormalParameters() throws Exception{ + Map dimensions = new HashMap<>(); + dimensions.put("key", "value"); + ParseTaskUtils.wait(ParseAnalytics.trackEventInBackground("test", dimensions)); + + verify(controller, times(1)).trackEventInBackground( + eq("test"), eq(dimensions), isNull(String.class)); + } + + @Test + public void testTrackEventInBackgroundNullCallback() throws Exception{ + Map dimensions = new HashMap<>(); + ParseAnalytics.trackEventInBackground("test", dimensions, null); + + verify(controller, times(1)).trackEventInBackground( + eq("test"), eq(dimensions), isNull(String.class)); + } + + + @Test + public void testTrackEventInBackgroundNormalCallback() throws Exception{ + final Map dimensions = new HashMap<>(); + dimensions.put("key", "value"); + final Semaphore done = new Semaphore(0); + ParseAnalytics.trackEventInBackground("test", dimensions, + new SaveCallback() { + @Override + public void done(ParseException e) { + assertNull(e); + done.release(); + } + }); + + // Make sure the callback is called + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).trackEventInBackground( + eq("test"), eq(dimensions), isNull(String.class)); + + final Semaphore doneAgain = new Semaphore(0); + ParseAnalytics.trackEventInBackground("test", new SaveCallback() { + @Override + public void done(ParseException e) { + assertNull(e); + doneAgain.release(); + } + }); + + // Make sure the callback is called + assertTrue(doneAgain.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).trackEventInBackground + (eq("test"), Matchers.>eq(null), isNull(String.class)); + } + + //endregion + + //region testTrackAppOpenedInBackground + + @Test + public void testTrackAppOpenedInBackgroundNullIntent() throws Exception { + ParseTaskUtils.wait(ParseAnalytics.trackAppOpenedInBackground(null)); + + verify(controller, times(1)).trackAppOpenedInBackground(isNull(String.class), + isNull(String.class)); + } + + @Test + public void testTrackAppOpenedInBackgroundEmptyIntent() throws Exception { + Intent intent = new Intent(); + ParseTaskUtils.wait(ParseAnalytics.trackAppOpenedInBackground(intent)); + + verify(controller, times(1)).trackAppOpenedInBackground(isNull(String.class), + isNull(String.class)); + } + + @Test + public void testTrackAppOpenedInBackgroundNormalIntent() throws Exception { + Intent intent = makeIntentWithParseData("test"); + ParseTaskUtils.wait(ParseAnalytics.trackAppOpenedInBackground(intent)); + + verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); + } + + @Test + public void testTrackAppOpenedInBackgroundDuplicatedIntent() throws Exception { + Intent intent = makeIntentWithParseData("test"); + ParseTaskUtils.wait(ParseAnalytics.trackAppOpenedInBackground(intent)); + + verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); + + ParseTaskUtils.wait(ParseAnalytics.trackAppOpenedInBackground(intent)); + + verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); + } + + @Test + public void testTrackAppOpenedInBackgroundNullCallback() throws Exception { + Intent intent = makeIntentWithParseData("test"); + ParseAnalytics.trackAppOpenedInBackground(intent, null); + + verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); + } + + @Test + public void testTrackAppOpenedInBackgroundNormalCallback() throws Exception { + Intent intent = makeIntentWithParseData("test"); + final Semaphore done = new Semaphore(0); + ParseAnalytics.trackAppOpenedInBackground(intent, new SaveCallback() { + @Override + public void done(ParseException e) { + assertNull(e); + done.release(); + } + }); + + // Make sure the callback is called + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); + } + + //endregion + + //region testGetPushHashFromIntent + + @Test + public void testGetPushHashFromIntentNullIntent() throws Exception { + String pushHash = ParseAnalytics.getPushHashFromIntent(null); + + assertEquals(null, pushHash); + } + + @Test + public void testGetPushHashFromIntentEmptyIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + JSONObject json = new JSONObject(); + json.put("push_hash_wrong_key", "test"); + bundle.putString("data_wrong_key", json.toString()); + intent.putExtras(bundle); + + String pushHash = ParseAnalytics.getPushHashFromIntent(intent); + + assertEquals(null, pushHash); + } + + @Test + public void testGetPushHashFromIntentEmptyPushHashIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + JSONObject json = new JSONObject(); + json.put("push_hash_wrong_key", "test"); + bundle.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, json.toString()); + intent.putExtras(bundle); + + String pushHash = ParseAnalytics.getPushHashFromIntent(intent); + + assertEquals("", pushHash); + } + + @Test + public void testGetPushHashFromIntentWrongPushHashIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, "error_data"); + intent.putExtras(bundle); + + String pushHash = ParseAnalytics.getPushHashFromIntent(intent); + + assertEquals(null, pushHash); + } + + @Test + public void testGetPushHashFromIntentNormalIntent() throws Exception { + Intent intent = makeIntentWithParseData("test"); + + String pushHash = ParseAnalytics.getPushHashFromIntent(intent); + + assertEquals("test", pushHash); + } + + //endregion + + private Intent makeIntentWithParseData(String pushHash) throws JSONException { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + JSONObject json = new JSONObject(); + json.put("push_hash", pushHash); + bundle.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, json.toString()); + intent.putExtras(bundle); + return intent; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java new file mode 100644 index 0000000..89769df --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Matchers; + +import java.util.HashMap; +import java.util.Map; + +import bolts.Task; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ParseAuthenticationManagerTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private ParseAuthenticationManager manager; + private ParseCurrentUserController controller; + private AuthenticationCallback provider; + + @Before + public void setUp() { + controller = mock(ParseCurrentUserController.class); + manager = new ParseAuthenticationManager(controller); + provider = mock(AuthenticationCallback.class); + } + + //region testRegister + + @Test + public void testRegisterMultipleShouldThrow() { + when(controller.getAsync(false)).thenReturn(Task.forResult(null)); + AuthenticationCallback provider2 = mock(AuthenticationCallback.class); + + manager.register("test_provider", provider); + + thrown.expect(IllegalStateException.class); + manager.register("test_provider", provider2); + } + + @Test + public void testRegisterAnonymous() { + manager.register("anonymous", mock(AuthenticationCallback.class)); + verifyNoMoreInteractions(controller); + } + + @Test + public void testRegister() { + ParseUser user = mock(ParseUser.class); + when(controller.getAsync(false)).thenReturn(Task.forResult(user)); + + manager.register("test_provider", provider); + verify(controller).getAsync(false); + verify(user).synchronizeAuthDataAsync("test_provider"); + } + + //endregion + + @Test + public void testRestoreAuthentication() throws ParseException { + when(controller.getAsync(false)).thenReturn(Task.forResult(null)); + when(provider.onRestore(Matchers.>any())) + .thenReturn(true); + manager.register("test_provider", provider); + + Map authData = new HashMap<>(); + ParseTaskUtils.wait(manager.restoreAuthenticationAsync("test_provider", authData)); + + verify(provider).onRestore(authData); + } + + @Test + public void testDeauthenticateAsync() throws ParseException { + when(controller.getAsync(false)).thenReturn(Task.forResult(null)); + manager.register("test_provider", provider); + + ParseTaskUtils.wait(manager.deauthenticateAsync("test_provider")); + + verify(provider).onRestore(null); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseByteArrayHttpBodyTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseByteArrayHttpBodyTest.java new file mode 100644 index 0000000..61b7c6b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseByteArrayHttpBodyTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class ParseByteArrayHttpBodyTest { + + @Test + public void testInitializeWithString() throws IOException { + String content = "content"; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(body.getContent())); + assertEquals(contentType, body.getContentType()); + assertEquals(7, body.getContentLength()); + } + + @Test + public void testInitializeWithByteArray() throws IOException { + byte[] content = {1, 1, 1, 1, 1}; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + assertArrayEquals(content, ParseIOUtils.toByteArray(body.getContent())); + assertEquals(contentType, body.getContentType()); + assertEquals(5, body.getContentLength()); + } + + @Test + public void testWriteTo() throws IOException { + String content = "content"; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + + // Check content + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + String contentAgain = output.toString(); + assertEquals(content, contentAgain); + + // No need to check whether content input stream is closed since it is a ByteArrayInputStream + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseClientConfigurationTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseClientConfigurationTest.java new file mode 100644 index 0000000..ec9559c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseClientConfigurationTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Bundle; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.net.URL; +import java.util.Collection; +import java.util.Iterator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseClientConfigurationTest { + + private final String serverUrl = "http://example.com/parse"; + private final String appId = "MyAppId"; + private final String clientKey = "MyClientKey"; + private final String PARSE_SERVER_URL = "com.parse.SERVER_URL"; + private final String PARSE_APPLICATION_ID = "com.parse.APPLICATION_ID"; + private final String PARSE_CLIENT_KEY = "com.parse.CLIENT_KEY"; + + @Test + public void testBuilder() { + Parse.Configuration.Builder builder = new Parse.Configuration.Builder(null); + builder.applicationId("foo"); + builder.clientKey("bar"); + builder.enableLocalDataStore(); + Parse.Configuration configuration = builder.build(); + + assertNull(configuration.context); + assertEquals(configuration.applicationId, "foo"); + assertEquals(configuration.clientKey, "bar"); + assertEquals(configuration.localDataStoreEnabled, true); + } + + @Test + public void testBuilderServerURL() { + Parse.Configuration.Builder builder = new Parse.Configuration.Builder(null); + builder.server("http://myserver.com/parse/"); + Parse.Configuration configuration = builder.build(); + assertEquals(configuration.server, "http://myserver.com/parse/"); + } + + @Test + public void testBuilderServerMissingSlashURL() { + Parse.Configuration.Builder builder = new Parse.Configuration.Builder(null); + builder.server("http://myserver.com/missingslash"); + Parse.Configuration configuration = builder.build(); + assertEquals(configuration.server, "http://myserver.com/missingslash/"); + } + + @Test + public void testConfigureFromManifest() throws Exception { + Bundle metaData = setupMockMetaData(); + when(metaData.getString(PARSE_SERVER_URL)).thenReturn(serverUrl); + when(metaData.getString(PARSE_APPLICATION_ID)).thenReturn(appId); + when(metaData.getString(PARSE_CLIENT_KEY)).thenReturn(clientKey); + + Parse.Configuration.Builder builder = new Parse.Configuration.Builder(RuntimeEnvironment.application); + Parse.Configuration config = builder.build(); + assertEquals(serverUrl + "/", config.server); + assertEquals(appId, config.applicationId); + assertEquals(clientKey, config.clientKey); + + verifyMockMetaData(metaData); + } + + @Test(expected = RuntimeException.class) + public void testConfigureFromManifestWithoutServer() throws Exception { + Bundle metaData = setupMockMetaData(); + when(metaData.getString(PARSE_SERVER_URL)).thenReturn(null); + when(metaData.getString(PARSE_APPLICATION_ID)).thenReturn(appId); + when(metaData.getString(PARSE_CLIENT_KEY)).thenReturn(clientKey); + + // RuntimeException due to serverUrl = null + Parse.initialize(RuntimeEnvironment.application); + } + + @Test(expected = RuntimeException.class) + public void testConfigureFromManifestWithoutAppId() throws Exception { + Bundle metaData = setupMockMetaData(); + when(metaData.getString(PARSE_SERVER_URL)).thenReturn(serverUrl); + when(metaData.getString(PARSE_APPLICATION_ID)).thenReturn(null); + when(metaData.getString(PARSE_CLIENT_KEY)).thenReturn(clientKey); + + // RuntimeException due to applicationId = null + Parse.initialize(RuntimeEnvironment.application); + } + + @Test + public void testConfigureFromManifestWithoutClientKey() throws Exception { + Bundle metaData = setupMockMetaData(); + when(metaData.getString(PARSE_SERVER_URL)).thenReturn(serverUrl); + when(metaData.getString(PARSE_APPLICATION_ID)).thenReturn(appId); + when(metaData.getString(PARSE_CLIENT_KEY)).thenReturn(null); + + Parse.initialize(RuntimeEnvironment.application); + assertEquals(new URL(serverUrl + "/"), ParseRESTCommand.server); + assertEquals(appId, ParsePlugins.get().applicationId()); + assertNull(ParsePlugins.get().clientKey()); + + verifyMockMetaData(metaData); + } + + private void verifyMockMetaData(Bundle metaData) throws Exception { + verify(metaData).getString(PARSE_SERVER_URL); + verify(metaData).getString(PARSE_APPLICATION_ID); + verify(metaData).getString(PARSE_CLIENT_KEY); + } + + private Bundle setupMockMetaData() throws Exception { + Bundle metaData = mock(Bundle.class); + RuntimeEnvironment.application.getApplicationInfo().metaData = metaData; + return metaData; + } + + private static boolean collectionsEqual(Collection a, Collection b) { + if (a.size() != b.size()) { + return false; + } + + Iterator iteratorA = a.iterator(); + Iterator iteratorB = b.iterator(); + for (; iteratorA.hasNext() && iteratorB.hasNext();) { + T objectA = iteratorA.next(); + T objectB = iteratorB.next(); + + if (objectA == null || objectB == null) { + if (objectA != objectB) { + return false; + } + continue; + } + + if (!objectA.equals(objectB)) { + return false; + } + } + + if (iteratorA.hasNext() || iteratorB.hasNext()) { + return false; + } + return true; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java new file mode 100644 index 0000000..907babe --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; + +import bolts.Task; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ParseCloudCodeControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testConstructor + + @Test + public void testConstructor() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + + assertSame(restClient, controller.restClient); + } + + //endregion + + //region testConvertCloudResponse + + @Test + public void testConvertCloudResponseNullResponse() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + + Object result = controller.convertCloudResponse(null); + + assertNull(result); + } + + @Test + public void testConvertCloudResponseJsonResponseWithResultField() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + JSONObject response = new JSONObject(); + response.put("result", "test"); + + Object result = controller.convertCloudResponse(response); + + assertThat(result, instanceOf(String.class)); + assertEquals("test", result); + } + + @Test + public void testConvertCloudResponseJsonArrayResponse() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + JSONArray response = new JSONArray(); + response.put(0, "test"); + response.put(1, true); + response.put(2, 2); + + Object result = controller.convertCloudResponse(response); + + assertThat(result, instanceOf(List.class)); + List listResult = (List)result; + assertEquals(3, listResult.size()); + assertEquals("test", listResult.get(0)); + assertEquals(true, listResult.get(1)); + assertEquals(2, listResult.get(2)); + } + + //endregion + + //region testCallFunctionInBackground + + @Test + public void testCallFunctionInBackgroundCommand() throws Exception { + // TODO(mengyan): Verify proper command is constructed + } + + @Test + public void testCallFunctionInBackgroundSuccessWithResult() throws Exception { + JSONObject json = new JSONObject(); + json.put("result", "test"); + String content = json.toString(); + + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) content.length()) + .setContent(new ByteArrayInputStream(content.getBytes())) + .build(); + + ParseHttpClient restClient = mockParseHttpClientWithReponse(mockResponse); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + + Task cloudCodeTask = controller.callFunctionInBackground( + "test", new HashMap(), "sessionToken"); + ParseTaskUtils.wait(cloudCodeTask); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertEquals("test", cloudCodeTask.getResult()); + } + + @Test + public void testCallFunctionInBackgroundSuccessWithoutResult() throws Exception { + JSONObject json = new JSONObject(); + String content = json.toString(); + + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) content.length()) + .setContent(new ByteArrayInputStream(content.getBytes())) + .build(); + + ParseHttpClient restClient = mockParseHttpClientWithReponse(mockResponse); + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + + Task cloudCodeTask = controller.callFunctionInBackground( + "test", new HashMap(), "sessionToken"); + ParseTaskUtils.wait(cloudCodeTask); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertNull(cloudCodeTask.getResult()); + } + + @Test + public void testCallFunctionInBackgroundFailure() throws Exception { + // TODO(mengyan): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + ParseCloudCodeController controller = new ParseCloudCodeController(restClient); + + Task cloudCodeTask = + controller.callFunctionInBackground("test", new HashMap(), "sessionToken"); + // Do not use ParseTaskUtils.wait() since we do not want to throw the exception + cloudCodeTask.waitForCompletion(); + + // TODO(mengyan): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(cloudCodeTask.isFaulted()); + Exception error = cloudCodeTask.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + } + + //endregion + + private ParseHttpClient mockParseHttpClientWithReponse(ParseHttpResponse response) + throws IOException { + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + return client; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudTest.java new file mode 100644 index 0000000..f5f35ec --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCloudTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import bolts.Task; + +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// For android.os.Looper +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseCloudTest extends ResetPluginsParseTest { + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseTestUtils.setTestParseUser(); + } + + //region testGetCloudCodeController + + @Test + public void testGetCloudCodeController() { + ParseCloudCodeController controller = mock(ParseCloudCodeController.class); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + + assertSame(controller, ParseCloud.getCloudCodeController()); + } + + //endregion + + //region testCallFunctions + + @Test + public void testCallFunctionAsync() throws Exception { + ParseCloudCodeController controller = mockParseCloudCodeControllerWithResponse("result"); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + Map parameters = new HashMap<>(); + parameters.put("key1", Arrays.asList(1, 2, 3)); + parameters.put("key2", "value1"); + + Task cloudCodeTask = ParseCloud.callFunctionInBackground("name", parameters); + ParseTaskUtils.wait(cloudCodeTask); + + verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), + isNull(String.class)); + assertTrue(cloudCodeTask.isCompleted()); + assertNull(cloudCodeTask.getError()); + assertThat(cloudCodeTask.getResult(), instanceOf(String.class)); + assertEquals("result", cloudCodeTask.getResult()); + } + + @Test + public void testCallFunctionSync() throws Exception { + ParseCloudCodeController controller = mockParseCloudCodeControllerWithResponse("result"); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + Map parameters = new HashMap<>(); + parameters.put("key1", Arrays.asList(1, 2, 3)); + parameters.put("key2", "value1"); + + Object result = ParseCloud.callFunction("name", parameters); + + verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), + isNull(String.class)); + assertThat(result, instanceOf(String.class)); + assertEquals("result", result); + } + + @Test + public void testCallFunctionNullCallback() throws Exception { + ParseCloudCodeController controller = mockParseCloudCodeControllerWithResponse("result"); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + Map parameters = new HashMap<>(); + parameters.put("key1", Arrays.asList(1, 2, 3)); + parameters.put("key2", "value1"); + + ParseCloud.callFunctionInBackground("name", parameters, null); + + verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), + isNull(String.class)); + } + + @Test + public void testCallFunctionNormalCallback() throws Exception { + ParseCloudCodeController controller = mockParseCloudCodeControllerWithResponse("result"); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + Map parameters = new HashMap<>(); + parameters.put("key1", Arrays.asList(1, 2, 3)); + parameters.put("key2", "value1"); + + final Semaphore done = new Semaphore(0); + ParseCloud.callFunctionInBackground("name", parameters, new FunctionCallback() { + @Override + public void done(Object result, ParseException e) { + assertNull(e); + assertThat(result, instanceOf(String.class)); + assertEquals("result", result); + done.release(); + } + }); + + // Make sure the callback is called + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), + isNull(String.class)); + } + + //endregion + + private ParseCloudCodeController mockParseCloudCodeControllerWithResponse(final T result) { + ParseCloudCodeController controller = mock(ParseCloudCodeController.class); + when(controller.callFunctionInBackground(anyString(), anyMap(), anyString())) + .thenReturn(Task.forResult(result)); + return controller; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCoderTest.java new file mode 100644 index 0000000..59ba76e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCoderTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; + +// For android.util.Base64 +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseCoderTest { + + @Test + public void testBytes() throws Exception { + // string of bytes, including some invalid UTF8 data + byte[] bytes = { 4, 8, 16, 32, -128, 0, 0, 0 }; + + ParseEncoder encoder = PointerEncoder.get(); + JSONObject json = (JSONObject) encoder.encode(bytes); + + ParseDecoder decoder = ParseDecoder.get(); + byte[] bytesAgain = (byte[]) decoder.decode(json); + + assertEquals(8, bytesAgain.length); + assertEquals(4, bytesAgain[0]); + assertEquals(8, bytesAgain[1]); + assertEquals(16, bytesAgain[2]); + assertEquals(32, bytesAgain[3]); + assertEquals(-128, bytesAgain[4]); + assertEquals(0, bytesAgain[5]); + assertEquals(0, bytesAgain[6]); + assertEquals(0, bytesAgain[7]); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigControllerTest.java new file mode 100644 index 0000000..04e429f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigControllerTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import bolts.Task; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ParseConfigControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + //region testConstructor + + @Test + public void testConstructor() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseCurrentConfigController currentConfigController = mock(ParseCurrentConfigController.class); + ParseConfigController controller = new ParseConfigController(restClient, + currentConfigController); + + assertSame(currentConfigController, controller.getCurrentConfigController()); + } + + //endregion + + //region testGetAsync + + @Test + public void testGetAsyncSuccess() throws Exception { + // Construct sample response from server + final Date date = new Date(); + final ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("image.png").url("http://yarr.com/image.png").build()); + final ParseGeoPoint geoPoint = new ParseGeoPoint(44.484, 26.029); + final List list = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + final Map map = new HashMap() {{ + put("first", "foo"); + put("second", "bar"); + put("third", "baz"); + }}; + + final Map sampleConfigParameters = new HashMap() {{ + put("string", "value"); + put("int", 42); + put("double", 0.2778); + put("trueBool", true); + put("falseBool", false); + put("date", date); + put("file", file); + put("geoPoint", geoPoint); + put("array", list); + put("object", map); + }}; + + JSONObject responseJson = new JSONObject() {{ + put("params", NoObjectsEncoder.get().encode(sampleConfigParameters)); + }}; + + // Make ParseConfigController and call getAsync + ParseHttpClient restClient = mockParseHttpClientWithResponse(responseJson, 200, "OK"); + ParseCurrentConfigController currentConfigController = mockParseCurrentConfigController(); + ParseConfigController configController = + new ParseConfigController(restClient, currentConfigController); + + Task configTask = configController.getAsync(null); + ParseConfig config = ParseTaskUtils.wait(configTask); + + // Verify httpClient is called + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + // Verify currentConfigController is called + verify(currentConfigController, times(1)).setCurrentConfigAsync(eq(config)); + // Verify ParseConfig we get, do not use ParseConfig getter to keep test separately + Map paramsAgain = config.getParams(); + assertEquals(10, paramsAgain.size()); + assertEquals("value", paramsAgain.get("string")); + assertEquals(42, paramsAgain.get("int")); + assertEquals(0.2778, paramsAgain.get("double")); + assertTrue((Boolean) paramsAgain.get("trueBool")); + assertFalse((Boolean) paramsAgain.get("falseBool")); + assertEquals(date, paramsAgain.get("date")); + ParseFile fileAgain = (ParseFile) paramsAgain.get("file"); + assertEquals(file.getUrl(), fileAgain.getUrl()); + assertEquals(file.getName(), fileAgain.getName()); + ParseGeoPoint geoPointAgain = (ParseGeoPoint) paramsAgain.get("geoPoint"); + assertEquals(geoPoint.getLatitude(), geoPointAgain.getLatitude(), 0.0000001); + assertEquals(geoPoint.getLongitude(), geoPointAgain.getLongitude(), 0.0000001); + List listAgain = (List) paramsAgain.get("array"); + assertArrayEquals(list.toArray(), listAgain.toArray()); + Map mapAgain = (Map) paramsAgain.get("object"); + assertEquals(map.size(), mapAgain.size()); + for (Map.Entry entry : map.entrySet()) { + assertEquals(entry.getValue(), mapAgain.get(entry.getKey())); + } + } + + @Test + public void testGetAsyncFailureWithConnectionFailure() throws Exception { + // TODO(mengyan): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + // Make ParseConfigController and call getAsync + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + ParseCurrentConfigController currentConfigController = mockParseCurrentConfigController(); + ParseConfigController configController = + new ParseConfigController(restClient, currentConfigController); + Task configTask = configController.getAsync(null); + // Do not use ParseTaskUtils.wait() since we do not want to throw the exception + configTask.waitForCompletion(); + + // Verify httpClient is tried enough times + // TODO(mengyan): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(configTask.isFaulted()); + Exception error = configTask.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + // Verify currentConfigController is not called + verify(currentConfigController, times(0)).setCurrentConfigAsync(any(ParseConfig.class)); + } + + //endregion + + private ParseCurrentConfigController mockParseCurrentConfigController() { + ParseCurrentConfigController currentConfigController = mock(ParseCurrentConfigController.class); + when(currentConfigController.setCurrentConfigAsync(any(ParseConfig.class))) + .thenReturn(Task.forResult(null)); + return currentConfigController; + } + + //TODO(mengyan) Create unit test helper and move all similar methods to the class + private ParseHttpClient mockParseHttpClientWithResponse(JSONObject content, int statusCode, + String reasonPhrase) throws IOException { + byte[] contentBytes = content.toString().getBytes(); + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(contentBytes)) + .setStatusCode(statusCode) + .setTotalSize(contentBytes.length) + .setContentType("application/json") + .build(); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + return client; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigTest.java new file mode 100644 index 0000000..c10e4f1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseConfigTest.java @@ -0,0 +1,1038 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import bolts.Task; + +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + + +// For android.os.Looper +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseConfigTest extends ResetPluginsParseTest { + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseTestUtils.setTestParseUser(); + } + + //region testConstructor + + @Test + public void testDefaultConstructor() { + ParseConfig config = new ParseConfig(); + + assertEquals(0, config.params.size()); + } + + @Test + public void testDecodeWithValidJsonObject() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + JSONObject json = new JSONObject(); + json.put("params", NoObjectsEncoder.get().encode(params)); + + ParseConfig config = ParseConfig.decode(json, ParseDecoder.get()); + assertEquals(1, config.params.size()); + assertEquals("value", config.params.get("string")); + } + + @Test(expected = RuntimeException.class) + public void testDecodeWithInvalidJsonObject() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + JSONObject json = new JSONObject(); + json.put("error", NoObjectsEncoder.get().encode(params)); + + ParseConfig.decode(json, ParseDecoder.get()); + } + + //endregion + + //region testGetInBackground + + @Test + public void testGetInBackgroundSuccess() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + + ParseConfig config = new ParseConfig(params); + ParseConfigController controller = mockParseConfigControllerWithResponse(config); + ParseCorePlugins.getInstance().registerConfigController(controller); + + Task configTask = ParseConfig.getInBackground(); + ParseTaskUtils.wait(configTask); + ParseConfig configAgain = configTask.getResult(); + + verify(controller, times(1)).getAsync(anyString()); + assertEquals(1, configAgain.params.size()); + assertEquals("value", configAgain.params.get("string")); + } + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Test + public void testGetInBackgroundFail() throws Exception { + ParseException exception = new ParseException(ParseException.CONNECTION_FAILED, "error"); + ParseConfigController controller = mockParseConfigControllerWithException(exception); + ParseCorePlugins.getInstance().registerConfigController(controller); + + Task configTask = ParseConfig.getInBackground(); + configTask.waitForCompletion(); + + verify(controller, times(1)).getAsync(anyString()); + assertThat(configTask.getError(), instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, + ((ParseException) configTask.getError()).getCode()); + assertEquals("error", configTask.getError().getMessage()); + } + + @Test + public void testGetInBackgroundWithCallbackSuccess() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + + ParseConfig config = new ParseConfig(params); + ParseConfigController controller = mockParseConfigControllerWithResponse(config); + ParseCorePlugins.getInstance().registerConfigController(controller); + + final Semaphore done = new Semaphore(0); + ParseConfig.getInBackground(new ConfigCallback() { + @Override + public void done(ParseConfig config, ParseException e) { + assertEquals(1, config.params.size()); + assertEquals("value", config.params.get("string")); + done.release(); + } + }); + + // Make sure the callback is called + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).getAsync(anyString()); + } + + @Test + public void testGetInBackgroundWithCallbackFail() throws Exception { + ParseException exception = new ParseException(ParseException.CONNECTION_FAILED, "error"); + ParseConfigController controller = mockParseConfigControllerWithException(exception); + ParseCorePlugins.getInstance().registerConfigController(controller); + + final Semaphore done = new Semaphore(0); + ParseConfig.getInBackground(new ConfigCallback() { + @Override + public void done(ParseConfig config, ParseException e) { + assertEquals(ParseException.CONNECTION_FAILED, e.getCode()); + assertEquals("error", e.getMessage()); + done.release(); + } + }); + + // Make sure the callback is called + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).getAsync(anyString()); + } + + @Test + public void testGetSyncSuccess() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + + ParseConfig config = new ParseConfig(params); + ParseConfigController controller = mockParseConfigControllerWithResponse(config); + ParseCorePlugins.getInstance().registerConfigController(controller); + + ParseConfig configAgain = ParseConfig.get(); + + verify(controller, times(1)).getAsync(anyString()); + assertEquals(1, configAgain.params.size()); + assertEquals("value", configAgain.params.get("string")); + } + + @Test + public void testGetSyncFail() { + ParseException exception = new ParseException(ParseException.CONNECTION_FAILED, "error"); + ParseConfigController controller = mockParseConfigControllerWithException(exception); + ParseCorePlugins.getInstance().registerConfigController(controller); + + try { + ParseConfig.get(); + fail("Should throw an exception"); + } catch (ParseException e) { + verify(controller, times(1)).getAsync(anyString()); + assertEquals(ParseException.CONNECTION_FAILED, e.getCode()); + assertEquals("error", e.getMessage()); + } + } + + //endregion + + //region testGetCurrentConfig + + @Test + public void testGetCurrentConfigSuccess() throws Exception { + final Map params = new HashMap<>(); + params.put("string", "value"); + + ParseConfig config = new ParseConfig(params); + ParseConfigController controller = new ParseConfigController(mock(ParseHttpClient.class), + mockParseCurrentConfigControllerWithResponse(config)); + ParseCorePlugins.getInstance().registerConfigController(controller); + + ParseConfig configAgain = ParseConfig.getCurrentConfig(); + assertSame(config, configAgain); + } + + @Test + public void testGetCurrentConfigFail() throws Exception { + ParseException exception = new ParseException(ParseException.CONNECTION_FAILED, "error"); + ParseConfigController controller = new ParseConfigController(mock(ParseHttpClient.class), + mockParseCurrentConfigControllerWithException(exception)); + ParseCorePlugins.getInstance().registerConfigController(controller); + + ParseConfig configAgain = ParseConfig.getCurrentConfig(); + // Make sure we get an empty ParseConfig + assertEquals(0, configAgain.getParams().size()); + } + + //endregion + + //region testGetBoolean + + @Test + public void testGetBooleanKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", true); + + ParseConfig config = new ParseConfig(params); + assertTrue(config.getBoolean("key")); + assertTrue(config.getBoolean("key", false)); + } + + @Test + public void testGetBooleanKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", true); + + ParseConfig config = new ParseConfig(params); + assertFalse(config.getBoolean("wrongKey")); + assertFalse(config.getBoolean("wrongKey", false)); + } + + @Test + public void testGetBooleanKeyExistValueNotBoolean() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertFalse(config.getBoolean("key")); + assertFalse(config.getBoolean("key", false)); + } + + //endregion + + //region testGetInt + + @Test + public void testGetIntKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 998); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getInt("key"), 998); + assertEquals(998, config.getInt("key", 100)); + } + + @Test + public void testGetIntKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 998); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getInt("wrongKey"), 0); + assertEquals(100, config.getInt("wrongKey", 100)); + } + + //endregion + + //region testGetDouble + + @Test + public void testGetDoubleKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 998.1); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getDouble("key"), 998.1, 0.0001); + assertEquals(998.1, config.getDouble("key", 100.1), 0.0001); + } + + @Test + public void testGetDoubleKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 998.1); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getDouble("wrongKey"), 0.0, 0.0001); + assertEquals(100.1, config.getDouble("wrongKey", 100.1), 0.0001); + } + + //endregion + + //region testGetLong + + @Test + public void testGetLongKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", (long)998); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getLong("key"), (long)998); + assertEquals((long)998, config.getLong("key", (long) 100)); + } + + @Test + public void testGetLongKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", (long)998); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getLong("wrongKey"), (long)0); + assertEquals((long)100, config.getLong("wrongKey", (long) 100)); + } + + //endregion + + //region testGet + + @Test + public void testGetKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", "value"); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.get("key"), "value"); + assertEquals("value", config.get("key", "haha")); + } + + @Test + public void testGetKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", "value"); + + ParseConfig config = new ParseConfig(params); + assertNull(config.get("wrongKey")); + assertEquals("haha", config.get("wrongKey", "haha")); + } + + @Test + public void testGetKeyExistValueNull() throws Exception { + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.get("key")); + assertNull(config.get("key", "haha")); + assertNull(config.get("keyAgain")); + assertNull(config.get("keyAgain", "haha")); + } + + //endregion + + //region testGetString + + @Test + public void testGetStringKeyExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", "value"); + + ParseConfig config = new ParseConfig(params); + assertEquals(config.getString("key"), "value"); + assertEquals("value", config.getString("key", "haha")); + } + + @Test + public void testGetStringKeyNotExist() throws Exception { + final Map params = new HashMap<>(); + params.put("key", "value"); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getString("wrongKey")); + assertEquals("haha", config.getString("wrongKey", "haha")); + } + + @Test + public void testGetStringKeyExistValueNotString() throws Exception { + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getString("key")); + assertEquals("haha", config.getString("key", "haha")); + } + + @Test + public void testGetStringKeyExistValueNull() throws Exception { + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getString("key")); + assertNull(config.getString("key", "haha")); + assertNull(config.getString("keyAgain")); + assertNull(config.getString("keyAgain", "haha")); + } + + //endregion + + //region testGetDate + + @Test + public void testGetDateKeyExist() throws Exception { + final Date date = new Date(); + date.setTime(10); + Date dateAgain = new Date(); + dateAgain.setTime(20); + final Map params = new HashMap<>(); + params.put("key", date); + + ParseConfig config = new ParseConfig(params); + assertEquals(date.getTime(), config.getDate("key").getTime()); + assertEquals(date.getTime(), config.getDate("key", dateAgain).getTime()); + } + + @Test + public void testGetDateKeyNotExist() throws Exception { + final Date date = new Date(); + date.setTime(10); + Date dateAgain = new Date(); + dateAgain.setTime(10); + final Map params = new HashMap<>(); + params.put("key", date); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getDate("wrongKey")); + assertSame(dateAgain, config.getDate("wrongKey", dateAgain)); + } + + @Test + public void testGetDateKeyExistValueNotDate() throws Exception { + Date date = new Date(); + date.setTime(20); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getDate("key")); + assertSame(date, config.getDate("key", date)); + } + + @Test + public void testGetDateKeyExistValueNull() throws Exception { + Date date = new Date(); + date.setTime(20); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getDate("key")); + assertNull(config.getDate("key", date)); + assertNull(config.getDate("keyAgain")); + assertNull(config.getDate("keyAgain", date)); + } + + //endregion + + //region testGetList + + @Test + public void testGetListKeyExist() throws Exception { + final List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add("baz"); + final List listAgain = new ArrayList<>(); + list.add("fooAgain"); + list.add("barAgain"); + list.add("bazAgain"); + final Map params = new HashMap<>(); + params.put("key", list); + + ParseConfig config = new ParseConfig(params); + assertArrayEquals(list.toArray(), config.getList("key").toArray()); + assertArrayEquals(list.toArray(), config.getList("key", listAgain).toArray()); + } + + @Test + public void testGetListKeyNotExist() throws Exception { + final List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add("baz"); + final List listAgain = new ArrayList<>(); + list.add("fooAgain"); + list.add("barAgain"); + list.add("bazAgain"); + final Map params = new HashMap<>(); + params.put("key", list); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getList("wrongKey")); + assertSame(listAgain, config.getList("wrongKey", listAgain)); + } + + @Test + public void testGetListKeyExistValueNotList() throws Exception { + final List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add("baz"); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getList("key")); + assertSame(list, config.getList("key", list)); + } + + @Test + public void testGetListKeyExistValueNull() throws Exception { + final List list = new ArrayList<>(); + list.add("fooAgain"); + list.add("barAgain"); + list.add("bazAgain"); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getList("key")); + assertNull(config.getList("key", list)); + assertNull(config.getList("keyAgain")); + assertNull(config.getList("keyAgain", list)); + } + + //endregion + + //region testGetNumber + + @Test + public void testGetNumberKeyExist() throws Exception { + final Number number = 1; + Number numberAgain = 2; + final Map params = new HashMap<>(); + params.put("key", number); + + ParseConfig config = new ParseConfig(params); + assertEquals(number, config.getNumber("key")); + assertEquals(number, config.getNumber("key", numberAgain)); + } + + @Test + public void testGetNumberKeyNotExist() throws Exception { + final Number number = 1; + Number numberAgain = 2; + final Map params = new HashMap<>(); + params.put("key", number); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getNumber("wrongKey")); + assertSame(numberAgain, config.getNumber("wrongKey", numberAgain)); + } + + @Test + public void testGetNumberKeyExistValueNotNumber() throws Exception { + Number number = 2; + final Map params = new HashMap<>(); + params.put("key", new ArrayList()); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getNumber("key")); + assertSame(number, config.getNumber("key", number)); + } + + @Test + public void testGetNumberKeyExistValueNull() throws Exception { + Number number = 2; + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getNumber("key")); + assertNull(config.getNumber("key", number)); + assertNull(config.getNumber("keyAgain")); + assertNull(config.getNumber("keyAgain", number)); + } + + //endregion + + //region testGetMap + + @Test + public void testGetMapKeyExist() throws Exception { + final Map map = new HashMap<>(); + map.put("first", "foo"); + map.put("second", "bar"); + map.put("third", "baz"); + Map mapAgain = new HashMap<>(); + mapAgain.put("firstAgain", "fooAgain"); + mapAgain.put("secondAgain", "barAgain"); + mapAgain.put("thirdAgain", "bazAgain"); + final Map params = new HashMap<>(); + params.put("key", map); + + ParseConfig config = new ParseConfig(params); + Map mapConfig = config.getMap("key"); + assertEquals(3, mapConfig.size()); + assertEquals("foo", mapConfig.get("first")); + assertEquals("bar", mapConfig.get("second")); + assertEquals("baz", mapConfig.get("third")); + assertSame(mapConfig, config.getMap("key", mapAgain)); + } + + @Test + public void testGetMapKeyNotExist() throws Exception { + final Map map = new HashMap<>(); + map.put("first", "foo"); + map.put("second", "bar"); + map.put("third", "baz"); + Map mapAgain = new HashMap<>(); + mapAgain.put("firstAgain", "fooAgain"); + mapAgain.put("secondAgain", "barAgain"); + mapAgain.put("thirdAgain", "bazAgain"); + final Map params = new HashMap<>(); + params.put("key", map); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getMap("wrongKey")); + assertSame(mapAgain, config.getMap("wrongKey", mapAgain)); + } + + @Test + public void testGetMapKeyExistValueNotMap() throws Exception { + Map map = new HashMap<>(); + map.put("firstAgain", "fooAgain"); + map.put("secondAgain", "barAgain"); + map.put("thirdAgain", "bazAgain"); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getMap("key")); + assertSame(map, config.getMap("key", map)); + } + + @Test + public void testGetMapKeyExistValueNull() throws Exception { + Map map = new HashMap<>(); + map.put("firstAgain", "fooAgain"); + map.put("secondAgain", "barAgain"); + map.put("thirdAgain", "bazAgain"); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getMap("key")); + assertNull(config.getMap("key", map)); + assertNull(config.getMap("keyAgain")); + assertNull(config.getMap("keyAgain", map)); + } + + //endregion + + //region testGetJsonObject + + @Test + public void testGetJsonObjectKeyExist() throws Exception { + final Map value = new HashMap<>(); + value.put("key", "value"); + final Map params = new HashMap<>(); + params.put("key", value); + + final JSONObject json = new JSONObject(value); + + ParseConfig config = new ParseConfig(params); + assertEquals(json, config.getJSONObject("key"), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(json, config.getJSONObject("key", new JSONObject()), + JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testGetJsonObjectKeyNotExist() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final JSONObject jsonAgain = new JSONObject(); + jsonAgain.put("keyAgain", "valueAgain"); + final Map params; + params = new HashMap<>(); + params.put("key", json); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONObject("wrongKey")); + //TODO(mengyan) ParseConfig.getJSONObject should return jsonAgain, but due to the error in + // ParseConfig.getJsonObject, this returns null right now. Revise when we correct the logic + // for ParseConfig.getJsonObject. + assertNull(config.getJSONObject("wrongKey", jsonAgain)); + } + + @Test + public void testGetJsonObjectKeyExistValueNotJsonObject() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONObject("key")); + //TODO(mengyan) ParseConfig.getJSONObject should return json, but due to the error in + // ParseConfig.getJsonObject, this returns null right now. Revise when we correct the logic + // for ParseConfig.getJsonObject. + assertNull(config.getJSONObject("key", json)); + } + + @Test + public void testGetJsonObjectKeyExistValueNull() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONObject("key")); + assertNull(config.getJSONObject("key", json)); + assertNull(config.getJSONObject("keyAgain")); + assertNull(config.getJSONObject("keyAgain", json)); + } + + //endregion + + //region testGetJsonArray + + @Test + public void testGetJsonArrayKeyExist() throws Exception { + final Map map = new HashMap<>(); + map.put("key", "value"); + final Map mapAgain = new HashMap<>(); + mapAgain.put("keyAgain", "valueAgain"); + final List> value = new ArrayList<>(); + value.add(map); + value.add(mapAgain); + final Map params = new HashMap<>(); + params.put("key", value); + + JSONArray jsonArray = new JSONArray(value); + + ParseConfig config = new ParseConfig(params); + assertEquals(jsonArray, config.getJSONArray("key"), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(jsonArray, config.getJSONArray("key", new JSONArray()), + JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testGetJsonArrayKeyNotExist() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final JSONObject jsonAgain = new JSONObject(); + jsonAgain.put("keyAgain", "valueAgain"); + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(0, json); + jsonArray.put(1, jsonAgain); + final JSONArray jsonArrayAgain = new JSONArray(); + jsonArray.put(0, jsonAgain); + jsonArray.put(1, json); + final Map params = new HashMap<>(); + params.put("key", jsonArray); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONArray("wrongKey")); + //TODO(mengyan) ParseConfig.getJSONArray should return default jsonArrayAgain, but due to the + // error in ParseConfig.getJSONArray, this returns null right now. Revise when we correct the + // logic for ParseConfig.getJSONArray. + assertNull(config.getJSONArray("wrongKey", jsonArrayAgain)); + } + + @Test + public void testGetJsonArrayKeyExistValueNotJsonObject() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final JSONObject jsonAgain = new JSONObject(); + jsonAgain.put("keyAgain", "valueAgain"); + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(0, json); + jsonArray.put(1, jsonAgain); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONArray("key")); + //TODO(mengyan) ParseConfig.getJSONArray should return default jsonArray, but due to the + // error in ParseConfig.getJSONArray, this returns null right now. Revise when we correct the + // logic for ParseConfig.getJSONArray. + assertNull(config.getJSONArray("key", jsonArray)); + } + + @Test + public void testGetJsonArrayKeyExistValueNull() throws Exception { + final JSONObject json = new JSONObject(); + json.put("key", "value"); + final JSONObject jsonAgain = new JSONObject(); + jsonAgain.put("keyAgain", "valueAgain"); + final JSONArray jsonArray = new JSONArray(); + jsonArray.put(0, json); + jsonArray.put(1, jsonAgain); + final Map params = new HashMap<>(); + params.put("key", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getJSONArray("key")); + assertNull(config.getJSONArray("key", jsonArray)); + assertNull(config.getJSONArray("keyAgain")); + assertNull(config.getJSONArray("keyAgain", jsonArray)); + } + + //endregion + + //region testGetParseGeoPoint + + @Test + public void testGetParseGeoPointKeyExist() throws Exception { + final ParseGeoPoint geoPoint = new ParseGeoPoint(44.484, 26.029); + ParseGeoPoint geoPointAgain = new ParseGeoPoint(45.484, 27.029); + final Map params = new HashMap<>(); + params.put("key", geoPoint); + + ParseConfig config = new ParseConfig(params); + ParseGeoPoint geoPointConfig = config.getParseGeoPoint("key"); + assertEquals(geoPoint.getLongitude(), geoPointConfig.getLongitude(), 0.0001); + assertEquals(geoPoint.getLatitude(), geoPointConfig.getLatitude(), 0.0001); + assertSame(geoPointConfig, config.getParseGeoPoint("key", geoPointAgain)); + } + + @Test + public void testGetParseGeoPointKeyNotExist() throws Exception { + final ParseGeoPoint geoPoint = new ParseGeoPoint(44.484, 26.029); + ParseGeoPoint geoPointAgain = new ParseGeoPoint(45.484, 27.029); + final Map params = new HashMap<>(); + params.put("key", geoPoint); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseGeoPoint("wrongKey")); + assertSame(geoPointAgain, config.getParseGeoPoint("wrongKey", geoPointAgain)); + } + + @Test + public void testGetParseGeoPointKeyExistValueNotParseGeoPoint() throws Exception { + ParseGeoPoint geoPoint = new ParseGeoPoint(45.484, 27.029); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseGeoPoint("key")); + assertSame(geoPoint, config.getParseGeoPoint("key", geoPoint)); + } + + @Test + public void testGetParseGeoPointKeyExistValueNull() throws Exception { + ParseGeoPoint geoPoint = new ParseGeoPoint(45.484, 27.029); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", null); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseGeoPoint("key")); + assertNull(config.getParseGeoPoint("key", geoPoint)); + assertNull(config.getParseGeoPoint("keyAgain")); + assertNull(config.getParseGeoPoint("keyAgain", geoPoint)); + } + + //endregion + + //region testGetParseFile + + @Test + public void testGetParseFileKeyExist() throws Exception { + final ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("image.png").url("http://yarr.com/image.png").build()); + ParseFile fileAgain = new ParseFile( + new ParseFile.State.Builder().name("file.txt").url("http://yarr.com/file.txt").build()); + final Map params = new HashMap<>(); + params.put("key", file); + + ParseConfig config = new ParseConfig(params); + ParseFile fileConfig = config.getParseFile("key"); + assertEquals(file.getName(), fileConfig.getName()); + assertEquals(file.getUrl(), fileConfig.getUrl()); + assertSame(fileConfig, config.getParseFile("key", fileAgain)); + } + + @Test + public void testGetParseFileKeyNotExist() throws Exception { + final ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("image.png").url("http://yarr.com/image.png").build()); + ParseFile fileAgain = new ParseFile( + new ParseFile.State.Builder().name("file.txt").url("http://yarr.com/file.txt").build()); + final Map params = new HashMap<>(); + params.put("key", file); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseFile("wrongKey")); + assertSame(fileAgain, config.getParseFile("wrongKey", fileAgain)); + } + + @Test + public void testGetParseFileKeyExistValueNotParseFile() throws Exception { + ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("file.txt").url("http://yarr.com/file.txt").build()); + final Map params = new HashMap<>(); + params.put("key", 1); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseFile("key")); + assertSame(file, config.getParseFile("key", file)); + } + + @Test + public void testGetParseFileKeyExistValueNull() throws Exception { + ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("file.txt").url("http://yarr.com/file.txt").build()); + final Map params = new HashMap<>(); + params.put("key", JSONObject.NULL); + params.put("keyAgain", JSONObject.NULL); + + ParseConfig config = new ParseConfig(params); + assertNull(config.getParseFile("key")); + assertNull(config.getParseFile("key", file)); + assertNull(config.getParseFile("keyAgain")); + assertNull(config.getParseFile("keyAgain", file)); + } + + //endregion + + //region testToString + + @Test + public void testToStringList() throws Exception { + final List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add("baz"); + + final Map params = new HashMap<>(); + params.put("list", list); + + ParseConfig config = new ParseConfig(params); + String configStr = config.toString(); + assertTrue(configStr.contains("ParseConfig")); + assertTrue(configStr.contains("list")); + assertTrue(configStr.contains("bar")); + assertTrue(configStr.contains("baz")); + assertTrue(configStr.contains("foo")); + } + + @Test + public void testToStringMap() throws Exception { + final Map map = new HashMap<>(); + map.put("first", "foo"); + map.put("second", "bar"); + map.put("third", "baz"); + final Map params = new HashMap<>(); + params.put("map", map); + + ParseConfig config = new ParseConfig(params); + String configStr = config.toString(); + assertTrue(configStr.contains("ParseConfig")); + assertTrue(configStr.contains("map")); + assertTrue(configStr.contains("second=bar")); + assertTrue(configStr.contains("third=baz")); + assertTrue(configStr.contains("first=foo")); + } + + @Test + public void testToStringParseGeoPoint() throws Exception { + final ParseGeoPoint geoPoint = new ParseGeoPoint(45.484, 27.029); + final Map params = new HashMap<>(); + params.put("geoPoint", geoPoint); + + ParseConfig config = new ParseConfig(params); + String configStr = config.toString(); + assertTrue(configStr.contains("ParseGeoPoint")); + assertTrue(configStr.contains("45.484")); + assertTrue(configStr.contains("27.029")); + } + + //endregion + + private ParseConfigController mockParseConfigControllerWithResponse(final ParseConfig result) { + ParseConfigController controller = mock(ParseConfigController.class); + when(controller.getAsync(anyString())) + .thenReturn(Task.forResult(result)); + return controller; + } + + private ParseConfigController mockParseConfigControllerWithException(Exception exception) { + ParseConfigController controller = mock(ParseConfigController.class); + when(controller.getAsync(anyString())) + .thenReturn(Task.forError(exception)); + return controller; + } + + private ParseCurrentConfigController mockParseCurrentConfigControllerWithResponse( + final ParseConfig result) { + ParseCurrentConfigController controller = mock(ParseCurrentConfigController.class); + when(controller.getCurrentConfigAsync()) + .thenReturn(Task.forResult(result)); + return controller; + } + + private ParseCurrentConfigController mockParseCurrentConfigControllerWithException( + Exception exception) { + ParseCurrentConfigController controller = mock(ParseCurrentConfigController.class); + when(controller.getCurrentConfigAsync()) + .thenReturn(Task.forError(exception)); + return controller; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCorePluginsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCorePluginsTest.java new file mode 100644 index 0000000..31d8a9a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCorePluginsTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.List; + +import bolts.Task; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseCorePluginsTest extends ResetPluginsParseTest { + + @Before + public void setUp() throws Exception { + super.setUp(); + Parse.Configuration configuration = new Parse.Configuration.Builder(null) + .applicationId("1234") + .build(); + ParsePlugins.initialize(null, configuration); + } + + @Test + public void testQueryControllerDefaultImpl() { + ParseQueryController controller = ParseCorePlugins.getInstance().getQueryController(); + assertThat(controller, instanceOf(CacheQueryController.class)); + } + + @Test + public void testRegisterQueryController() { + ParseQueryController controller = new TestQueryController(); + ParseCorePlugins.getInstance().registerQueryController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getQueryController()); + } + + @Test(expected = IllegalStateException.class) + public void testRegisterQueryControllerWhenAlreadySet() { + ParseCorePlugins.getInstance().getQueryController(); // sets to default + ParseQueryController controller = new TestQueryController(); + ParseCorePlugins.getInstance().registerQueryController(controller); + } + + //TODO(grantland): testFileControllerDefaultImpl with ParseFileController interface + + @Test + public void testRegisterFileController() { + ParseFileController controller = new TestFileController(); + ParseCorePlugins.getInstance().registerFileController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getFileController()); + } + + //TODO(grantland): testRegisterFileControllerWhenAlreadySet when getCacheDir is no longer global + + //TODO(mengyan): testAnalyticsControllerDefaultImpl when getEventuallyQueue is no longer global + + @Test + public void testRegisterAnalyticsController() { + ParseAnalyticsController controller = new TestAnalyticsController(); + ParseCorePlugins.getInstance().registerAnalyticsController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getAnalyticsController()); + } + + //TODO(mengyan): testRegisterAnalyticsControllerWhenAlreadySet when getEventuallyQueue is no longer global + + @Test + public void testCloudCodeControllerDefaultImpl() { + ParseCloudCodeController controller = ParseCorePlugins.getInstance().getCloudCodeController(); + assertThat(controller, instanceOf(ParseCloudCodeController.class)); + } + + @Test + public void testRegisterCloudCodeController() { + ParseCloudCodeController controller = new TestCloudCodeController(); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getCloudCodeController()); + } + + @Test(expected = IllegalStateException.class) + public void testRegisterCloudCodeControllerWhenAlreadySet() { + ParseCorePlugins.getInstance().getCloudCodeController(); // sets to default + ParseCloudCodeController controller = new TestCloudCodeController(); + ParseCorePlugins.getInstance().registerCloudCodeController(controller); + } + + // TODO(mengyan): testConfigControllerDefaultImpl when getCacheDir is no longer global + + @Test + public void testRegisterConfigController() { + ParseConfigController controller = new TestConfigController(); + ParseCorePlugins.getInstance().registerConfigController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getConfigController()); + } + + // TODO(mengyan): testRegisterConfigControllerWhenAlreadySet when getCacheDir is no longer global + + @Test + public void testPushControllerDefaultImpl() { + ParsePushController controller = ParseCorePlugins.getInstance().getPushController(); + assertThat(controller, instanceOf(ParsePushController.class)); + } + + @Test + public void testRegisterPushController() { + ParsePushController controller = new TestPushController(); + ParseCorePlugins.getInstance().registerPushController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getPushController()); + } + + @Test(expected = IllegalStateException.class) + public void testRegisterPushControllerWhenAlreadySet() { + ParseCorePlugins.getInstance().getPushController(); // sets to default + ParsePushController controller = new TestPushController(); + ParseCorePlugins.getInstance().registerPushController(controller); + } + + public void testPushChannelsControllerDefaultImpl() { + ParsePushChannelsController controller = + ParseCorePlugins.getInstance().getPushChannelsController(); + assertThat(controller, instanceOf(ParsePushChannelsController.class)); + } + + @Test + public void testRegisterPushChannelsController() { + ParsePushChannelsController controller = new ParsePushChannelsController(); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + assertSame(controller, ParseCorePlugins.getInstance().getPushChannelsController()); + } + + @Test(expected = IllegalStateException.class) + public void testRegisterPushChannelsControllerWhenAlreadySet() { + ParseCorePlugins.getInstance().getPushChannelsController(); // sets to default + ParsePushChannelsController controller = new ParsePushChannelsController(); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + } + + private static class TestQueryController implements ParseQueryController { + @Override + public Task> findAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + return null; + } + + @Override + public Task countAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + return null; + } + + @Override + public Task getFirstAsync( + ParseQuery.State state, ParseUser user, Task cancellationToken) { + return null; + } + } + + private static class TestFileController extends ParseFileController { + public TestFileController() { + super(null, null); + } + } + + private static class TestAnalyticsController extends ParseAnalyticsController { + public TestAnalyticsController() { + super(null); + } + } + + private static class TestCloudCodeController extends ParseCloudCodeController { + public TestCloudCodeController() { + super(null); + } + } + + private static class TestConfigController extends ParseConfigController { + public TestConfigController() { + super(null, null); + } + } + + private static class TestPushController extends ParsePushController { + public TestPushController() { + super(null); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java new file mode 100644 index 0000000..a35092f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ParseCountingByteArrayHttpBodyTest { + + @Test + public void testWriteTo() throws IOException, InterruptedException { + byte[] content = getData(); + String contentType = "application/json"; + + final Semaphore didReportIntermediateProgress = new Semaphore(0); + final Semaphore finish = new Semaphore(0); + + ParseCountingByteArrayHttpBody body = new ParseCountingByteArrayHttpBody( + content, contentType, new ProgressCallback() { + Integer maxProgressSoFar = 0; + @Override + public void done(Integer percentDone) { + if (percentDone > maxProgressSoFar) { + maxProgressSoFar = percentDone; + assertTrue(percentDone >= 0 && percentDone <= 100); + + if (percentDone < 100 && percentDone > 0) { + didReportIntermediateProgress.release(); + } else if (percentDone == 100) { + finish.release(); + } else if (percentDone == 0) { + // do nothing + } else { + fail("percentDone should be within 0 - 100"); + } + } + } + }); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + + // Check content + assertTrue(Arrays.equals(content, output.toByteArray())); + + // Check progress callback + assertTrue(didReportIntermediateProgress.tryAcquire(5, TimeUnit.SECONDS)); + assertTrue(finish.tryAcquire(5, TimeUnit.SECONDS)); + } + + private static byte[] getData() { + char[] chars = new char[64 << 14]; // 1MB + Arrays.fill(chars, '1'); + return new String(chars).getBytes(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java new file mode 100644 index 0000000..e58d923 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ParseCountingFileHttpBodyTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testWriteTo() throws Exception { + final Semaphore didReportIntermediateProgress = new Semaphore(0); + final Semaphore finish = new Semaphore(0); + + ParseCountingFileHttpBody body = new ParseCountingFileHttpBody( + makeTestFile(temporaryFolder.getRoot()), new ProgressCallback() { + Integer maxProgressSoFar = 0; + @Override + public void done(Integer percentDone) { + if (percentDone > maxProgressSoFar) { + maxProgressSoFar = percentDone; + assertTrue(percentDone >= 0 && percentDone <= 100); + + if (percentDone < 100 && percentDone > 0) { + didReportIntermediateProgress.release(); + } else if (percentDone == 100) { + finish.release(); + } else if (percentDone == 0) { + // do nothing + } else { + fail("percentDone should be within 0 - 100"); + } + } + } + }); + + // Check content + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + assertArrayEquals(getData().getBytes(), output.toByteArray()); + // Check progress callback + assertTrue(didReportIntermediateProgress.tryAcquire(5, TimeUnit.SECONDS)); + assertTrue(finish.tryAcquire(5, TimeUnit.SECONDS)); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteToWithNullOutput() throws Exception { + ParseCountingFileHttpBody body = new ParseCountingFileHttpBody( + makeTestFile(temporaryFolder.getRoot()), null); + body.writeTo(null); + } + + private static String getData() { + char[] chars = new char[64 << 14]; // 1MB + Arrays.fill(chars, '1'); + return new String(chars); + } + + private static File makeTestFile(File root) throws IOException { + File file = new File(root, "test"); + FileWriter writer = new FileWriter(file); + writer.write(getData()); + writer.close(); + return file; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java new file mode 100644 index 0000000..4eba223 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import bolts.Task; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ParseCurrentConfigControllerTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + //region testConstructor + + @Test + public void testConstructor() { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + + assertNull(currentConfigController.currentConfig); + } + + //endregion + + //region testSaveToDisk + + @Test + public void testSaveToDiskSuccess() throws Exception { + // Construct sample ParseConfig + final Date date = new Date(); + final ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("image.png").url("http://yarr.com/image.png").build()); + final ParseGeoPoint geoPoint = new ParseGeoPoint(44.484, 26.029); + final List list = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + final Map map = new HashMap() {{ + put("first", "foo"); + put("second", "bar"); + put("third", "baz"); + }}; + + final Map sampleConfigParameters = new HashMap() {{ + put("string", "value"); + put("int", 42); + put("double", 0.2778); + put("trueBool", true); + put("falseBool", false); + put("date", date); + put("file", file); + put("geoPoint", geoPoint); + put("array", list); + put("object", map); + }}; + + JSONObject sampleConfigJson = new JSONObject() {{ + put("params", NoObjectsEncoder.get().encode(sampleConfigParameters)); + }}; + ParseConfig config = ParseConfig.decode(sampleConfigJson, ParseDecoder.get()); + + // Save to disk + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + currentConfigController.saveToDisk(config); + + // Verify file on disk + JSONObject diskConfigJson = + new JSONObject(ParseFileUtils.readFileToString(configFile, "UTF-8")); + Map decodedDiskConfigObject = + (Map) ParseDecoder.get().decode(diskConfigJson); + Map decodedDiskConfigParameters = + (Map) decodedDiskConfigObject.get("params"); + assertEquals(10, decodedDiskConfigParameters.size()); + assertEquals("value", decodedDiskConfigParameters.get("string")); + assertEquals(42, decodedDiskConfigParameters.get("int")); + assertEquals(0.2778, decodedDiskConfigParameters.get("double")); + assertTrue((Boolean) decodedDiskConfigParameters.get("trueBool")); + assertFalse((Boolean) decodedDiskConfigParameters.get("falseBool")); + assertEquals(date, decodedDiskConfigParameters.get("date")); + ParseFile fileAgain = (ParseFile) decodedDiskConfigParameters.get("file"); + assertEquals(file.getUrl(), fileAgain.getUrl()); + assertEquals(file.getName(), fileAgain.getName()); + ParseGeoPoint geoPointAgain = (ParseGeoPoint) decodedDiskConfigParameters.get("geoPoint"); + assertEquals(geoPoint.getLatitude(), geoPointAgain.getLatitude(), 0.0000001); + assertEquals(geoPoint.getLongitude(), geoPointAgain.getLongitude(), 0.0000001); + List listAgain = (List) decodedDiskConfigParameters.get("array"); + assertArrayEquals(list.toArray(), listAgain.toArray()); + Map mapAgain = (Map) decodedDiskConfigParameters.get("object"); + assertEquals(map.size(), mapAgain.size()); + for (Map.Entry entry : map.entrySet()) { + assertEquals(entry.getValue(), mapAgain.get(entry.getKey())); + } + } + + //TODO(mengyan) Add testSaveToDiskFailIOException when we have a way to handle IOException + + //endregion + + //region testGetFromDisk + + @Test + public void testGetFromDiskSuccess() throws Exception { + // Construct sample ParseConfig json + final Date date = new Date(); + final ParseFile file = new ParseFile( + new ParseFile.State.Builder().name("image.png").url("http://yarr.com/image.png").build()); + final ParseGeoPoint geoPoint = new ParseGeoPoint(44.484, 26.029); + final List list = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + final Map map = new HashMap() {{ + put("first", "foo"); + put("second", "bar"); + put("third", "baz"); + }}; + + final Map sampleConfigParameters = new HashMap() {{ + put("string", "value"); + put("int", 42); + put("double", 0.2778); + put("trueBool", true); + put("falseBool", false); + put("date", date); + put("file", file); + put("geoPoint", geoPoint); + put("array", list); + put("object", map); + }}; + + JSONObject sampleConfigJson = new JSONObject() {{ + put("params", NoObjectsEncoder.get().encode(sampleConfigParameters)); + }}; + ParseConfig config = ParseConfig.decode(sampleConfigJson, ParseDecoder.get()); + + // Save to disk + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + currentConfigController.saveToDisk(config); + + // Verify ParseConfig we get from getFromDisk + ParseConfig configAgain = currentConfigController.getFromDisk(); + Map paramsAgain = configAgain.getParams(); + assertEquals(10, paramsAgain.size()); + assertEquals("value", paramsAgain.get("string")); + assertEquals(42, paramsAgain.get("int")); + assertEquals(0.2778, paramsAgain.get("double")); + assertTrue((Boolean) paramsAgain.get("trueBool")); + assertFalse((Boolean) paramsAgain.get("falseBool")); + assertEquals(date, paramsAgain.get("date")); + ParseFile fileAgain = (ParseFile) paramsAgain.get("file"); + assertEquals(file.getUrl(), fileAgain.getUrl()); + assertEquals(file.getName(), fileAgain.getName()); + ParseGeoPoint geoPointAgain = (ParseGeoPoint) paramsAgain.get("geoPoint"); + assertEquals(geoPoint.getLatitude(), geoPointAgain.getLatitude(), 0.0000001); + assertEquals(geoPoint.getLongitude(), geoPointAgain.getLongitude(), 0.0000001); + List listAgain = (List) paramsAgain.get("array"); + assertArrayEquals(list.toArray(), listAgain.toArray()); + Map mapAgain = (Map) paramsAgain.get("object"); + assertEquals(map.size(), mapAgain.size()); + for (Map.Entry entry : map.entrySet()) { + assertEquals(entry.getValue(), mapAgain.get(entry.getKey())); + } + } + + @Test + public void testGetFromDiskConfigSuccessFileIOException() { + File configFile = new File("errorConfigFile"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + ParseConfig config = currentConfigController.getFromDisk(); + assertNull(config); + } + + @Test + public void testGetFromDiskSuccessConfigFileNotJsonFile() throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseFileUtils.writeStringToFile(configFile, "notJson", "UTF-8"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + ParseConfig config = currentConfigController.getFromDisk(); + assertNull(config); + } + + //endregion + + //region testSetCurrentConfigAsync + + @Test + public void testSetCurrentConfigAsyncSuccess() throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + + // Verify before set, file is empty and in memory config is null + assertFalse(configFile.exists()); + assertNull(currentConfigController.currentConfig); + + ParseConfig config = new ParseConfig(); + ParseTaskUtils.wait(currentConfigController.setCurrentConfigAsync(config)); + + // Verify after set, file exists(saveToDisk is called) and in memory config is set + assertTrue(configFile.exists()); + assertSame(config, currentConfigController.currentConfig); + } + + //endregion + + //region testGetCurrentConfigAsync + + @Test + public void testGetCurrentConfigAsyncSuccessCurrentConfigAlreadySet() throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + ParseConfig config = new ParseConfig(); + currentConfigController.currentConfig = config; + + Task getTask = currentConfigController.getCurrentConfigAsync(); + ParseTaskUtils.wait(getTask); + ParseConfig configAgain = getTask.getResult(); + + // Verify we get the same ParseConfig when currentConfig is set + assertSame(config, configAgain); + } + + @Test + public void testGetCurrentConfigAsyncSuccessCurrentConfigNotSetDiskConfigExist() + throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + + // Save sample ParseConfig to disk + final Map sampleConfigParameters = new HashMap() {{ + put("string", "value"); + }}; + JSONObject sampleConfigJson = new JSONObject() {{ + put("params", NoObjectsEncoder.get().encode(sampleConfigParameters)); + }}; + ParseConfig diskConfig = ParseConfig.decode(sampleConfigJson, ParseDecoder.get()); + currentConfigController.saveToDisk(diskConfig); + + // Verify before set, disk config exist and in memory config is null + assertTrue(configFile.exists()); + assertNull(currentConfigController.currentConfig); + + Task getTask = currentConfigController.getCurrentConfigAsync(); + ParseTaskUtils.wait(getTask); + ParseConfig config = getTask.getResult(); + + // Verify after set, in memory config is set and value is correct + assertSame(config, currentConfigController.currentConfig); + assertEquals("value", config.get("string")); + assertEquals(1, config.getParams().size()); + } + + @Test + public void testGetCurrentConfigAsyncSuccessCurrentConfigNotSetDiskConfigNotExist() + throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + + // Verify before set, disk config does not exist and in memory config is null + assertFalse(configFile.exists()); + assertNull(currentConfigController.currentConfig); + + Task getTask = currentConfigController.getCurrentConfigAsync(); + ParseTaskUtils.wait(getTask); + ParseConfig config = getTask.getResult(); + + // Verify after set, in memory config is set and the config we get is empty + assertSame(config, currentConfigController.currentConfig); + assertEquals(0, config.getParams().size()); + } + + //endregion + + //region testClearCurrentConfigForTesting + + @Test + public void testClearCurrentConfigForTestingSuccess() throws Exception { + File configFile = new File(temporaryFolder.getRoot(), "config"); + ParseCurrentConfigController currentConfigController = + new ParseCurrentConfigController(configFile); + currentConfigController.currentConfig = new ParseConfig(); + + // Verify before set, in memory config is not null + assertNotNull(currentConfigController.currentConfig); + + currentConfigController.clearCurrentConfigForTesting(); + + // Verify after set, in memory config is null + assertNull(currentConfigController.currentConfig); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDateFormatTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDateFormatTest.java new file mode 100644 index 0000000..508c3ae --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDateFormatTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ParseDateFormatTest { + @Test + public void testParse() { + String string = "2015-05-13T11:08:01.123Z"; + Date date = ParseDateFormat.getInstance().parse(string); + assertEquals(1431515281123L, date.getTime()); + } + + @Test + public void testParseInvalid() { + String string = "2015-05-13T11:08:01Z"; + Date date = ParseDateFormat.getInstance().parse(string); + assertNull(date); + } + + @Test + public void testFormat() { + Date date = new Date(1431515281123L); + String string = ParseDateFormat.getInstance().format(date); + assertEquals("2015-05-13T11:08:01.123Z", string); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDecoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDecoderTest.java new file mode 100644 index 0000000..5efb5de --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDecoderTest.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +// For android.util.Base64 +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseDecoderTest extends ResetPluginsParseTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testJSONArray() { + JSONArray jsonArray = new JSONArray(); + List list = (List) ParseDecoder.get().decode(jsonArray); + assertNotNull(list); + } + + @Test + public void testConvertJSONArrayToList() { + JSONArray jsonArray = new JSONArray(); + jsonArray.put("Object1"); + + JSONArray internalJSONArray = new JSONArray(); + internalJSONArray.put("Object2.1"); + internalJSONArray.put("Object2.2"); + jsonArray.put(internalJSONArray); + + List objects = ParseDecoder.get().convertJSONArrayToList(jsonArray); + assertNotNull(objects); + assertEquals(2, objects.size()); + assertEquals("Object1", objects.get(0)); + + List subObjects = (List) objects.get(1); + assertNotNull(subObjects); + assertEquals(2, subObjects.size()); + assertEquals("Object2.1", subObjects.get(0)); + assertEquals("Object2.2", subObjects.get(1)); + } + + @Test + public void testNonJSONObject() { + Object obj = new Object(); + assertSame(obj, ParseDecoder.get().decode(obj)); + } + + @Test + public void testNull() { + Object object = ParseDecoder.get().decode(JSONObject.NULL); + assertNull(object); + } + + @Test + public void testParseFieldOperations() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__op", "Increment"); + json.put("amount", 1); + ParseFieldOperations.registerDefaultDecoders(); + Object obj = ParseDecoder.get().decode(json); + assertEquals("ParseIncrementOperation", obj.getClass().getSimpleName()); + } + + @Test + public void testMap() throws JSONException { + JSONObject json = new JSONObject(); + json.put("score", 3); + json.put("age", 33); + Map keyValueMap = (Map) ParseDecoder.get().decode(json); + assertNotNull(keyValueMap); + } + + @Test + public void testConvertJSONObjectToMap() throws JSONException { + JSONObject json = new JSONObject(); + json.put("Total Score", 20); + + JSONObject internalJSON = new JSONObject(); + internalJSON.put("Innings 1", 10); + internalJSON.put("Innings 2", 10); + json.put("Innings Scores", internalJSON); + + Map scoreMap = (Map) ParseDecoder.get().decode(json); + assertNotNull(scoreMap); + assertEquals(2, scoreMap.size()); + assertEquals(20, scoreMap.get("Total Score")); + + Map inningsMap = (Map) scoreMap.get("Innings Scores"); + assertNotNull(inningsMap); + assertEquals(2, inningsMap.size()); + assertEquals(10, inningsMap.get("Innings 1")); + assertEquals(10, inningsMap.get("Innings 2")); + } + + @Test + public void testDate() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Date"); + json.put("iso", "2011-08-21T18:02:52.249Z"); + Date date = (Date) ParseDecoder.get().decode(json); + assertNotNull(date); + } + + @Test + public void testBytes() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Bytes"); + json.put("base64", "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw=="); + byte[] byteArr = (byte[]) ParseDecoder.get().decode(json); + assertEquals("This is an encoded string", new String(byteArr)); + } + + @Test + public void testPointer() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Pointer"); + json.put("className", "GameScore"); + json.put("objectId", "Ed1nuqPvc"); + ParseObject pointerObject = (ParseObject) ParseDecoder.get().decode(json); + assertNotNull(pointerObject); + assertFalse(pointerObject.isDataAvailable()); + } + + @Test + public void testFile() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "File"); + json.put("name", "image-file.png"); + json.put("url", "http://folder/image-file.png"); + ParseFile parseFile = (ParseFile) ParseDecoder.get().decode(json); + assertNotNull(parseFile); + } + + @Test + public void testGeoPointWithoutLongitude() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "GeoPoint"); + json.put("latitude", 30); + + thrown.expect(RuntimeException.class); + ParseDecoder.get().decode(json); + } + + @Test + public void testGeoPointWithoutLatitude() throws JSONException { + + JSONObject json = new JSONObject(); + json.put("__type", "GeoPoint"); + json.put("longitude", 30); + + thrown.expect(RuntimeException.class); + ParseDecoder.get().decode(json); + } + + @Test + public void testGeoPoint() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "GeoPoint"); + json.put("longitude", -20); + json.put("latitude", 30); + + ParseGeoPoint parseGeoPoint = (ParseGeoPoint) ParseDecoder.get().decode(json); + assertNotNull(parseGeoPoint); + final double DELTA = 0.00001; + assertEquals(-20, parseGeoPoint.getLongitude(), DELTA); + assertEquals(30, parseGeoPoint.getLatitude(), DELTA); + } + + @Test + public void testPolygonWithoutCoordinates() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Polygon"); + + thrown.expect(RuntimeException.class); + ParseDecoder.get().decode(json); + } + + @Test + public void testPolygon() throws JSONException { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParsePolygon polygon = new ParsePolygon(points); + + JSONObject json = new JSONObject(); + json.put("__type", "Polygon"); + json.put("coordinates", polygon.coordinatesToJSONArray()); + + ParsePolygon parsePolygon = (ParsePolygon) ParseDecoder.get().decode(json); + assertNotNull(parsePolygon); + assertEquals(polygon.getCoordinates(), parsePolygon.getCoordinates()); + } + + @Test + public void testParseObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertNotNull(parseObject); + } + + @Test + public void testIncludedParseObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + JSONObject child = new JSONObject(); + child.put("__type", "Object"); + child.put("className", "GameScore"); + child.put("createdAt", "2015-06-22T21:23:41.733Z"); + child.put("objectId", "TT1ZskATqR"); + child.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + json.put("child", child); + ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertNotNull(parseObject.getParseObject("child")); + } + + @Test + public void testCompleteness() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + json.put("foo", "foo"); + json.put("bar", "bar"); + ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertTrue(parseObject.isDataAvailable()); + + JSONArray arr = new JSONArray("[\"foo\"]"); + json.put("__selectedKeys", arr); + parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertFalse(parseObject.isDataAvailable()); + } + + @Test + public void testCompletenessOfIncludedParseObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + JSONObject child = new JSONObject(); + child.put("__type", "Object"); + child.put("className", "GameScore"); + child.put("createdAt", "2015-06-22T21:23:41.733Z"); + child.put("objectId", "TT1ZskATqR"); + child.put("updatedAt", "2015-06-22T22:06:18.104Z"); + child.put("bar", "child bar"); + + JSONArray arr = new JSONArray("[\"foo.bar\"]"); + json.put("foo", child); + json.put("__selectedKeys", arr); + ParseObject parentObject = (ParseObject) ParseDecoder.get().decode(json); + assertFalse(parentObject.isDataAvailable()); + assertTrue(parentObject.isDataAvailable("foo")); + ParseObject childObject = parentObject.getParseObject("foo"); + assertFalse(childObject.isDataAvailable()); + assertTrue(childObject.isDataAvailable("bar")); + } + + @Test + public void testRelation() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Relation"); + json.put("className", "Player"); + json.put("objectId", "TT1ZskATqS"); + ParseRelation parseRelation = + (ParseRelation) ParseDecoder.get().decode(json); + assertNotNull(parseRelation); + } + + @Test + public void testOfflineObject() throws JSONException { + thrown.expect(RuntimeException.class); + thrown.expectMessage("An unexpected offline pointer was encountered."); + + + JSONObject json = new JSONObject(); + json.put("__type", "OfflineObject"); + ParseDecoder.get().decode(json); + } + + @Test + public void testMisc() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "TestType"); + assertNull(ParseDecoder.get().decode(json)); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java new file mode 100644 index 0000000..9dc3b2c --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.ref.WeakReference; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ParseDefaultACLControllerTest { + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseRole.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseRole.class); + ParseCorePlugins.getInstance().reset(); + } + + //region testSetDefaultACL + + @Test + public void testSetDefaultACLWithACL() { + ParseACL acl = mock(ParseACL.class); + ParseACL copiedACL = mock(ParseACL.class); + when(acl.copy()).thenReturn(copiedACL); + ParseDefaultACLController controller = new ParseDefaultACLController(); + + controller.set(acl, true); + + assertNull(controller.defaultACLWithCurrentUser); + assertNull(controller.lastCurrentUser); + assertTrue(controller.defaultACLUsesCurrentUser); + verify(copiedACL, times(1)).setShared(true); + assertEquals(copiedACL, controller.defaultACL); + } + + @Test + public void testSetDefaultACLWithNull() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + + controller.set(null, true); + + assertNull(controller.defaultACLWithCurrentUser); + assertNull(controller.lastCurrentUser); + assertNull(controller.defaultACL); + } + + //endregion + + //region testGetDefaultACL + + @Test + public void testGetDefaultACLWithNoDefaultACL() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + + ParseACL defaultACL = controller.get(); + + assertNull(defaultACL); + } + + @Test + public void testGetDefaultACLWithNoDefaultACLUsesCurrentUser() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + ParseACL acl = new ParseACL(); + controller.defaultACL = acl; + controller.defaultACLUsesCurrentUser = false; + + ParseACL defaultACL = controller.get(); + + assertSame(acl, defaultACL); + } + + @Test + public void testGetDefaultACLWithNoCurrentUser() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + ParseACL acl = new ParseACL(); + controller.defaultACL = acl; + controller.defaultACLUsesCurrentUser = true; + // Register currentUser + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseACL defaultACL = controller.get(); + + assertSame(acl, defaultACL); + } + + @Test + public void testGetDefaultACLWithSameCurrentUserAndLastCurrentUser() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + ParseACL acl = new ParseACL(); + controller.defaultACL = acl; + controller.defaultACLUsesCurrentUser = true; + ParseACL aclAgain = new ParseACL(); + controller.defaultACLWithCurrentUser = aclAgain; + ParseUser currentUser = mock(ParseUser.class); + controller.lastCurrentUser = new WeakReference<>(currentUser); + // Register currentUser + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseACL defaultACL = controller.get(); + + assertNotSame(acl, defaultACL); + assertSame(aclAgain, defaultACL); + } + + @Test + public void testGetDefaultACLWithCurrentUserAndLastCurrentUserNotSame() { + ParseDefaultACLController controller = new ParseDefaultACLController(); + ParseACL acl = mock(ParseACL.class); + ParseACL copiedACL = mock(ParseACL.class); + when(acl.copy()).thenReturn(copiedACL); + controller.defaultACL = acl; + controller.defaultACLUsesCurrentUser = true; + ParseACL aclAgain = new ParseACL(); + controller.defaultACLWithCurrentUser = aclAgain; + // Register currentUser + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + ParseUser currentUser = mock(ParseUser.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseACL defaultACL = controller.get(); + + verify(copiedACL, times(1)).setShared(true); + verify(copiedACL, times(1)).setReadAccess(eq(currentUser), eq(true)); + verify(copiedACL, times(1)).setWriteAccess(eq(currentUser), eq(true)); + assertSame(currentUser, controller.lastCurrentUser.get()); + assertNotSame(acl, defaultACL); + assertSame(copiedACL, defaultACL); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDigestUtilsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDigestUtilsTest.java new file mode 100644 index 0000000..9727cad --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseDigestUtilsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ParseDigestUtilsTest { + + @Test + public void testMD5() { + Map stringToMD5 = new HashMap<>(); + stringToMD5.put("grantland", "66ad19754cd2edcc20c3221a5488a599"); + stringToMD5.put("nikita", "b00a50c448238a71ed479f81fa4d9066"); + stringToMD5.put("1337", "e48e13207341b6bffb7fb1622282247b"); + stringToMD5.put("I am a potato", "2e832e16f60587842c7e4080142dbeca"); + + for (Map.Entry entry : stringToMD5.entrySet()) { + String md5 = ParseDigestUtils.md5(entry.getKey()); + assertEquals(entry.getValue(), md5); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseEncoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseEncoderTest.java new file mode 100644 index 0000000..dafee8a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseEncoderTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Date; +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +// For android.util.Base64 +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseEncoderTest { + + ParseEncoderTestClass testClassObject = null; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() { + testClassObject = new ParseEncoderTestClass(); + } + + @Test + public void testQueryStateBuilder() throws JSONException { + ParseQuery.State.Builder stateBuilder = + new ParseQuery.State.Builder<>("TestObject"); + JSONObject stateJSON = (JSONObject) testClassObject.encode(stateBuilder); + assertNotNull(stateJSON); + assertEquals("TestObject", stateJSON.getString("className")); + } + + @Test + public void testQueryState() throws JSONException { + ParseQuery.State state = + (new ParseQuery.State.Builder<>("TestObject")).build(); + JSONObject stateJSON = (JSONObject) testClassObject.encode(state); + assertNotNull(stateJSON); + assertEquals("TestObject", stateJSON.getString("className")); + } + + @Test + public void testDate() throws JSONException { + Date date = ParseDateFormat.getInstance().parse("2011-08-21T18:02:52.249Z"); + JSONObject dateJSON = (JSONObject) testClassObject.encode(date); + assertNotNull(dateJSON); + assertEquals("Date", dateJSON.getString("__type")); + assertEquals("2011-08-21T18:02:52.249Z", dateJSON.getString("iso")); + } + + @Test + public void testBytes() throws JSONException { + byte[] byteArr = "This is an encoded string".getBytes(); + JSONObject bytesJSON = (JSONObject) testClassObject.encode(byteArr); + assertNotNull(bytesJSON); + assertEquals("Bytes", bytesJSON.getString("__type")); + assertEquals("VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==", bytesJSON.getString("base64")); + } + + @Test + public void testParseFile() throws JSONException { + ParseFile.State.Builder parseFileStateBuilder = new ParseFile.State.Builder(); + parseFileStateBuilder.name("image-file.png"); + parseFileStateBuilder.url("http://folder/image-file.png"); + ParseFile parseFile = new ParseFile(parseFileStateBuilder.build()); + JSONObject fileJSON = (JSONObject) testClassObject.encode(parseFile); + assertNotNull(fileJSON); + assertEquals("File", fileJSON.getString("__type")); + assertEquals("image-file.png", fileJSON.getString("name")); + assertEquals("http://folder/image-file.png", fileJSON.getString("url")); + } + + @Test + public void testParseGeoPoint() throws JSONException { + ParseGeoPoint parseGeoPoint = new ParseGeoPoint(30, -20); + JSONObject geoPointJSON = (JSONObject) testClassObject.encode(parseGeoPoint); + assertNotNull(geoPointJSON); + final double DELTA = 0.00001; + assertEquals("GeoPoint", geoPointJSON.getString("__type")); + assertEquals(30, geoPointJSON.getDouble("latitude"), DELTA); + assertEquals(-20, geoPointJSON.getDouble("longitude"), DELTA); + } + + @Test + public void testParsePolygon() throws JSONException { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParsePolygon parsePolygon = new ParsePolygon(points); + JSONObject polygonJSON = (JSONObject) testClassObject.encode(parsePolygon); + assertNotNull(polygonJSON); + assertEquals("Polygon", polygonJSON.getString("__type")); + assertEquals(parsePolygon.coordinatesToJSONArray(), polygonJSON.getJSONArray("coordinates")); + } + + @Test + public void testParseACL() throws JSONException { + ParseACL parseACL = new ParseACL(); + JSONObject aclJSON = (JSONObject) testClassObject.encode(parseACL); + assertNotNull(aclJSON); + } + + @Test + public void testMap() throws JSONException { + HashMap map = new HashMap<>(); + map.put("key1", "object1"); + map.put("key2", "object2"); + JSONObject mapJSON = (JSONObject) testClassObject.encode(map); + assertNotNull(mapJSON); + assertEquals(2, mapJSON.length()); + assertEquals("object1", mapJSON.getString("key1")); + assertEquals("object2", mapJSON.getString("key2")); + } + + @Test + public void testCollection() throws JSONException { + ArrayList list = new ArrayList<>(); + list.add(1); + list.add(2); + JSONArray jsonArray = (JSONArray) testClassObject.encode(list); + assertNotNull(jsonArray); + assertEquals(2, jsonArray.length()); + assertEquals(1, jsonArray.get(0)); + assertEquals(2, jsonArray.get(1)); + } + + @Test + public void testRelation() throws JSONException { + ParseRelation parseRelation = new ParseRelation("TestObject"); + JSONObject relationJSON = (JSONObject) testClassObject.encode(parseRelation); + assertNotNull(relationJSON); + assertEquals("Relation", relationJSON.getString("__type")); + assertEquals("TestObject", relationJSON.getString("className")); + } + + @Test + public void testParseFieldOperations() throws JSONException { + ParseIncrementOperation incrementOperation = new ParseIncrementOperation(2); + JSONObject incrementJSON = (JSONObject) testClassObject.encode(incrementOperation); + assertNotNull(incrementJSON); + assertEquals("Increment", incrementJSON.getString("__op")); + assertEquals(2, incrementJSON.getInt("amount")); + } + + @Test + public void testRelationContraint() throws JSONException { + ParseObject parseObject = new ParseObject("TestObject"); + ParseQuery.RelationConstraint relationConstraint = + new ParseQuery.RelationConstraint(">", parseObject); + JSONObject relationConstraintJSON = (JSONObject) testClassObject.encode(relationConstraint); + assertNotNull(relationConstraintJSON); + assertEquals(">", relationConstraintJSON.getString("key")); + } + + @Test + public void testNull() throws JSONException { + Object object = testClassObject.encode(null); + assertEquals(object, JSONObject.NULL); + } + + @Test + public void testPrimitives() throws JSONException { + String encodedStr = (String) testClassObject.encode("String"); + assertEquals(encodedStr, "String"); + int encodedInteger = (Integer) testClassObject.encode(5); + assertEquals(5, encodedInteger); + boolean encodedBoolean = (Boolean) testClassObject.encode(true); + assertTrue(encodedBoolean); + final double DELTA = 0.00001; + double encodedDouble = (Double) testClassObject.encode(5.5); + assertEquals(5.5, encodedDouble, DELTA); + } + + @Test + public void testIllegalArgument() throws JSONException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("invalid type for ParseObject: " + + ParseDecoder.class.toString()); + testClassObject.encode(ParseDecoder.get()); + } + + private static class ParseEncoderTestClass extends ParseEncoder { + @Override + protected JSONObject encodeRelatedObject(ParseObject object) { + return null; + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileControllerTest.java new file mode 100644 index 0000000..4dd0eed --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import bolts.Task; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// For org.json +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseFileControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + // TODO(grantland): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(ParseRequest.DEFAULT_INITIAL_RETRY_DELAY); + ParseRESTCommand.server = null; + } + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testGetCacheFile() throws Exception { + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(null, root); + + ParseFile.State state = new ParseFile.State.Builder().name("test_file").build(); + File cacheFile = controller.getCacheFile(state); + assertEquals(new File(root, "test_file"), cacheFile); + } + + @Test + public void testIsDataAvailable() throws IOException { + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(null, root); + + temporaryFolder.newFile("test_file"); + + ParseFile.State state = new ParseFile.State.Builder().name("test_file").build(); + assertTrue(controller.isDataAvailable(state)); + } + + @Test + public void testClearCache() throws IOException { + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(null, root); + + File file1 = temporaryFolder.newFile("test_file_1"); + File file2 = temporaryFolder.newFile("test_file_2"); + controller.clearCache(); + assertFalse(file1.exists()); + assertFalse(file2.exists()); + } + + //region testSaveAsync + + @Test + public void testSaveAsyncRequest() throws Exception { + // TODO(grantland): Verify proper command is constructed + } + + @Test + public void testSaveAsyncNotDirty() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseFileController controller = new ParseFileController(restClient, null); + + ParseFile.State state = new ParseFile.State.Builder() + .url("http://example.com") + .build(); + Task task = controller.saveAsync(state, (byte[])null, null, null, null); + task.waitForCompletion(); + + verify(restClient, times(0)).execute(any(ParseHttpRequest.class)); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertSame(state, task.getResult()); + } + + @Test + public void testSaveAsyncAlreadyCancelled() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParseFileController controller = new ParseFileController(restClient, null); + + ParseFile.State state = new ParseFile.State.Builder().build(); + Task cancellationToken = Task.cancelled(); + Task task = controller.saveAsync(state, (byte[])null, null, null, cancellationToken); + task.waitForCompletion(); + + verify(restClient, times(0)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isCancelled()); + } + + @Test + public void testSaveAsyncSuccessWithByteArray() throws Exception { + JSONObject json = new JSONObject(); + json.put("name", "new_file_name"); + json.put("url", "http://example.com"); + String content = json.toString(); + + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) content.length()) + .setContent(new ByteArrayInputStream(content.getBytes())) + .build(); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + byte[] data = "hello".getBytes(); + ParseFile.State state = new ParseFile.State.Builder() + .name("file_name") + .mimeType("mime_type") + .build(); + Task task = controller.saveAsync(state, data, null, null, null); + ParseFile.State result = ParseTaskUtils.wait(task); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertEquals("new_file_name", result.name()); + assertEquals("http://example.com", result.url()); + File file = new File(root, "new_file_name"); + assertTrue(file.exists()); + assertEquals("hello", ParseFileUtils.readFileToString(file, "UTF-8")); + } + + @Test + public void testSaveAsyncSuccessWithFile() throws Exception { + JSONObject json = new JSONObject(); + json.put("name", "new_file_name"); + json.put("url", "http://example.com"); + String content = json.toString(); + + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) content.length()) + .setContent(new ByteArrayInputStream(content.getBytes())) + .build(); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + File file = new File(root, "test"); + ParseFileUtils.writeStringToFile(file, "content", "UTF-8"); + ParseFile.State state = new ParseFile.State.Builder() + .name("file_name") + .mimeType("mime_type") + .build(); + Task task = controller.saveAsync(state, file, null, null, null); + ParseFile.State result = ParseTaskUtils.wait(task); + + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + assertEquals("new_file_name", result.name()); + assertEquals("http://example.com", result.url()); + File cachedFile = new File(root, "new_file_name"); + assertTrue(cachedFile.exists()); + assertTrue(file.exists()); + assertEquals("content", ParseFileUtils.readFileToString(cachedFile, "UTF-8")); + } + + @Test + public void testSaveAsyncFailureWithByteArray() throws Exception { + // TODO(grantland): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + byte[] data = "hello".getBytes(); + ParseFile.State state = new ParseFile.State.Builder() + .build(); + Task task = controller.saveAsync(state, data, null, null, null); + task.waitForCompletion(); + + // TODO(grantland): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isFaulted()); + Exception error = task.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + assertEquals(0, root.listFiles().length); + } + + @Test + public void testSaveAsyncFailureWithFile() throws Exception { + // TODO(grantland): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(restClient, root); + + File file = temporaryFolder.newFile("test"); + ParseFile.State state = new ParseFile.State.Builder() + .build(); + Task task = controller.saveAsync(state, file, null, null, null); + task.waitForCompletion(); + + // TODO(grantland): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isFaulted()); + Exception error = task.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + // Make sure the original file is not deleted and there is no cache file in the folder + assertEquals(1, root.listFiles().length); + assertTrue(file.exists()); + } + + //endregion + + //region testFetchAsync + + @Test + public void testFetchAsyncRequest() { + // TODO(grantland): Verify proper command is constructed + } + + @Test + public void testFetchAsyncAlreadyCancelled() throws Exception{ + ParseHttpClient fileClient = mock(ParseHttpClient.class); + ParseFileController controller = new ParseFileController(null, null).fileClient(fileClient); + + ParseFile.State state = new ParseFile.State.Builder().build(); + Task cancellationToken = Task.cancelled(); + Task task = controller.fetchAsync(state, null, null, cancellationToken); + task.waitForCompletion(); + + verify(fileClient, times(0)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isCancelled()); + } + + @Test + public void testFetchAsyncCached() throws Exception { + ParseHttpClient fileClient = mock(ParseHttpClient.class); + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(null, root).fileClient(fileClient); + + File file = new File(root, "cached_file_name"); + ParseFileUtils.writeStringToFile(file, "hello", "UTF-8"); + + ParseFile.State state = new ParseFile.State.Builder() + .name("cached_file_name") + .build(); + Task task = controller.fetchAsync(state, null, null, null); + File result = ParseTaskUtils.wait(task); + + verify(fileClient, times(0)).execute(any(ParseHttpRequest.class)); + assertEquals(file, result); + assertEquals("hello", ParseFileUtils.readFileToString(result, "UTF-8")); + } + + @Test + public void testFetchAsyncSuccess() throws Exception { + byte[] data = "hello".getBytes(); + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); + + ParseHttpClient fileClient = mock(ParseHttpClient.class); + when(fileClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + // Make sure cache dir does not exist + File root = new File(temporaryFolder.getRoot(), "cache"); + assertFalse(root.exists()); + ParseFileController controller = new ParseFileController(null, root).fileClient(fileClient); + + ParseFile.State state = new ParseFile.State.Builder() + .name("file_name") + .url("url") + .build(); + Task task = controller.fetchAsync(state, null, null, null); + File result = ParseTaskUtils.wait(task); + + verify(fileClient, times(1)).execute(any(ParseHttpRequest.class)); + assertTrue(result.exists()); + assertEquals("hello", ParseFileUtils.readFileToString(result, "UTF-8")); + assertFalse(controller.getTempFile(state).exists()); + } + + @Test + public void testFetchAsyncFailure() throws Exception { + // TODO(grantland): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient fileClient = mock(ParseHttpClient.class); + when(fileClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + File root = temporaryFolder.getRoot(); + ParseFileController controller = new ParseFileController(null, root).fileClient(fileClient); + + // We need to set url to make getTempFile() work and check it + ParseFile.State state = new ParseFile.State.Builder() + .url("test") + .build(); + Task task = controller.fetchAsync(state, null, null, null); + task.waitForCompletion(); + + // TODO(grantland): Abstract out command runner so we don't have to account for retries. + verify(fileClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(task.isFaulted()); + Exception error = task.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + assertEquals(0, root.listFiles().length); + assertFalse(controller.getTempFile(state).exists()); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileHttpBodyTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileHttpBodyTest.java new file mode 100644 index 0000000..02c94b1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileHttpBodyTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ParseFileHttpBodyTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testInitializeWithFileAndContentType() throws IOException { + String contentType = "text/plain"; + File file = makeTestFile(temporaryFolder.getRoot()); + + ParseFileHttpBody body = new ParseFileHttpBody(file, contentType); + + assertEquals(file.length(), body.getContentLength()); + assertEquals(contentType, body.getContentType()); + // Verify file content + InputStream content = body.getContent(); + byte[] contentBytes = ParseIOUtils.toByteArray(content); + ParseIOUtils.closeQuietly(content); + verifyTestFileContent(contentBytes); + } + + @Test + public void testInitializeWithFile() throws IOException { + File file = makeTestFile(temporaryFolder.getRoot()); + + ParseFileHttpBody body = new ParseFileHttpBody(file); + + assertEquals(file.length(), body.getContentLength()); + assertNull(body.getContentType()); + // Verify file content + InputStream content = body.getContent(); + byte[] contentBytes = ParseIOUtils.toByteArray(content); + ParseIOUtils.closeQuietly(content); + verifyTestFileContent(contentBytes); + } + + @Test + public void testWriteTo() throws IOException { + File file = makeTestFile(temporaryFolder.getRoot()); + ParseFileHttpBody body = new ParseFileHttpBody(file); + + // Check content + ByteArrayOutputStream output = new ByteArrayOutputStream(); + body.writeTo(output); + verifyTestFileContent(output.toByteArray()); + } + + @Test(expected = IllegalArgumentException.class) + public void testWriteToWithNullOutput() throws Exception { + ParseFileHttpBody body = new ParseFileHttpBody(makeTestFile(temporaryFolder.getRoot())); + body.writeTo(null); + } + + // Generate a test file used for create ParseFileHttpBody, if you change file's content, make sure + // you also change the test file content in verifyTestFileContent(). + private static File makeTestFile(File root) throws IOException { + File file = new File(root, "test"); + String content = "content"; + FileWriter writer = new FileWriter(file); + writer.write(content); + writer.close(); + return file; + } + + private static void verifyTestFileContent(byte[] bytes) throws IOException { + assertArrayEquals("content".getBytes(), bytes); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileRequestTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileRequestTest.java new file mode 100644 index 0000000..3896efc --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileRequestTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import bolts.Task; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ParseFileRequestTest extends TestCase { + + @Override + protected void tearDown() throws Exception { + ParseRequest.setDefaultInitialRetryDelay(ParseRequest.DEFAULT_INITIAL_RETRY_DELAY); + super.tearDown(); + } + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + public void test4XXThrowsException() throws Exception { + ParseRequest.setDefaultInitialRetryDelay(1L); + InputStream mockInputStream = new ByteArrayInputStream( + "An Error occurred while saving".getBytes()); + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(400) + .setTotalSize(0L) + .setReasonPhrase("Bad Request") + .setContent(mockInputStream) + .build(); + + ParseHttpClient mockHttpClient = mock(ParseHttpClient.class); + when(mockHttpClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + + ParseFileRequest request = + new ParseFileRequest(ParseHttpRequest.Method.GET, "http://parse.com", null); + Task task = request.executeAsync(mockHttpClient); + task.waitForCompletion(); + + assertTrue(task.isFaulted()); + assertTrue(task.getError() instanceof ParseException); + ParseException error = (ParseException) task.getError(); + assertEquals(error.getCode(), ParseException.CONNECTION_FAILED); + assertTrue(error.getMessage().contains("Download from file server")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileStateTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileStateTest.java new file mode 100644 index 0000000..8bf463b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileStateTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.webkit.MimeTypeMap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.robolectric.Shadows.shadowOf; + +// For android.webkit.MimeTypeMap +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseFileStateTest { + + @Before + public void setUp() { + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("txt", "text/plain"); + } + + @After + public void tearDown() { + shadowOf(MimeTypeMap.getSingleton()).clearMappings(); + } + + @Test + public void testDefaults() { + ParseFile.State state = new ParseFile.State.Builder().build(); + assertEquals("file", state.name()); + assertEquals(null, state.mimeType()); + assertNull(state.url()); + } + + @Test + public void testProperties() { + ParseFile.State state = new ParseFile.State.Builder() + .name("test") + .mimeType("application/test") + .url("http://twitter.com/grantland") + .build(); + assertEquals("test", state.name()); + assertEquals("application/test", state.mimeType()); + assertEquals("http://twitter.com/grantland", state.url()); + } + + @Test + public void testCopy() { + ParseFile.State state = new ParseFile.State.Builder() + .name("test") + .mimeType("application/test") + .url("http://twitter.com/grantland") + .build(); + ParseFile.State copy = new ParseFile.State.Builder(state).build(); + assertEquals("test", copy.name()); + assertEquals("application/test", copy.mimeType()); + assertEquals("http://twitter.com/grantland", copy.url()); + assertNotSame(state, copy); + } + + @Test + public void testMimeType() { + ParseFile.State state = new ParseFile.State.Builder() + .mimeType("test") + .build(); + assertEquals("test", state.mimeType()); + } + + @Test + public void testMimeTypeNotSetFromExtension() { + ParseFile.State state = new ParseFile.State.Builder() + .name("test.txt") + .build(); + assertEquals(null, state.mimeType()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileTest.java new file mode 100644 index 0000000..1328af1 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileTest.java @@ -0,0 +1,531 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +import bolts.Task; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseFileTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setup() { + ParseCorePlugins.getInstance().reset(); + ParseTestUtils.setTestParseUser(); + } + + @After + public void tearDown() { + ParseCorePlugins.getInstance().reset(); + } + + @Test + public void testConstructor() throws Exception { + String name = "name"; + byte[] data = "hello".getBytes(); + String contentType = "content_type"; + File file = temporaryFolder.newFile(name); + + // TODO(mengyan): After we have proper staging strategy, we should verify the staging file's + // content is the same with the original file. + + ParseFile parseFile = new ParseFile(name, data, contentType); + assertEquals("name", parseFile.getName()); + assertEquals("content_type", parseFile.getState().mimeType()); + assertTrue(parseFile.isDirty()); + + parseFile = new ParseFile(data); + assertEquals("file", parseFile.getName()); // Default + assertEquals(null, parseFile.getState().mimeType()); + assertTrue(parseFile.isDirty()); + + parseFile = new ParseFile(name, data); + assertEquals("name", parseFile.getName()); + assertEquals(null, parseFile.getState().mimeType()); + assertTrue(parseFile.isDirty()); + + parseFile = new ParseFile(data, contentType); + assertEquals("file", parseFile.getName()); // Default + assertEquals("content_type", parseFile.getState().mimeType()); + assertTrue(parseFile.isDirty()); + + parseFile = new ParseFile(file); + assertEquals(name, parseFile.getName()); // Default + assertEquals(null, parseFile.getState().mimeType()); + assertTrue(parseFile.isDirty()); + + parseFile = new ParseFile(file, contentType); + assertEquals(name, parseFile.getName()); // Default + assertEquals("content_type", parseFile.getState().mimeType()); + } + + @Test + public void testGetters() { + ParseFile file = new ParseFile(new ParseFile.State.Builder().url("http://example.com").build()); + assertEquals("http://example.com", file.getUrl()); + assertFalse(file.isDirty()); + + // Note: rest of the getters are tested in `testConstructor` + } + + @Test + public void testIsDataAvailableCachedInMemory() { + ParseFile file = new ParseFile(new ParseFile.State.Builder().build()); + file.data = "hello".getBytes(); + assertTrue(file.isDataAvailable()); + } + + @Test + public void testIsDataAvailableCachedInController() { + ParseFileController controller = mock(ParseFileController.class); + when(controller.isDataAvailable(any(ParseFile.State.class))).thenReturn(true); + + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile.State state = new ParseFile.State.Builder().build(); + ParseFile file = new ParseFile(state); + + assertTrue(file.isDataAvailable()); + verify(controller).isDataAvailable(state); + } + + //region testSaveAsync + + @Test + public void testSaveAsyncNotDirty() throws Exception { + ParseFileController controller = mock(ParseFileController.class); + when(controller.isDataAvailable(any(ParseFile.State.class))).thenReturn(true); + + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile.State state = new ParseFile.State.Builder().url("http://example.com").build(); + ParseFile file = new ParseFile(state); + + Task task = file.saveAsync(null, null, null); + ParseTaskUtils.wait(task); + + verify(controller, never()).saveAsync( + any(ParseFile.State.class), + any(byte[].class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any()); + } + + @Test + public void testSaveAsyncCancelled() throws Exception { + ParseFileController controller = mock(ParseFileController.class); + when(controller.isDataAvailable(any(ParseFile.State.class))).thenReturn(true); + + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile.State state = new ParseFile.State.Builder().build(); + ParseFile file = new ParseFile(state); + + Task task = file.saveAsync(null, null, Task.cancelled()); + task.waitForCompletion(); + assertTrue(task.isCancelled()); + + verify(controller, never()).saveAsync( + any(ParseFile.State.class), + any(byte[].class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any()); + } + + @Test + public void testSaveAsyncSuccessWithData() throws Exception { + String name = "name"; + byte[] data = "hello".getBytes(); + String contentType = "content_type"; + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFileController controller = mock(ParseFileController.class); + when(controller.saveAsync( + any(ParseFile.State.class), + any(byte[].class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(state)); + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile parseFile = new ParseFile(name, data, contentType); + ParseTaskUtils.wait(parseFile.saveAsync(null, null, null)); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(byte[].class); + verify(controller, times(1)).saveAsync( + stateCaptor.capture(), + dataCaptor.capture(), + any(String.class), + any(ProgressCallback.class), + Matchers.>any()); + assertNull(stateCaptor.getValue().url()); + assertEquals(name, stateCaptor.getValue().name()); + assertEquals(contentType, stateCaptor.getValue().mimeType()); + assertArrayEquals(data, dataCaptor.getValue()); + // Verify the state of ParseFile has been updated + assertEquals(url, parseFile.getUrl()); + } + + @Test + public void testSaveAsyncSuccessWithFile() throws Exception { + String name = "name"; + File file = temporaryFolder.newFile(name); + String contentType = "content_type"; + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFileController controller = mock(ParseFileController.class); + when(controller.saveAsync( + any(ParseFile.State.class), + any(File.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(state)); + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile parseFile = new ParseFile(file, contentType); + ParseTaskUtils.wait(parseFile.saveAsync(null, null, null)); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + verify(controller, times(1)).saveAsync( + stateCaptor.capture(), + fileCaptor.capture(), + any(String.class), + any(ProgressCallback.class), + Matchers.>any()); + assertNull(stateCaptor.getValue().url()); + assertEquals(name, stateCaptor.getValue().name()); + assertEquals(contentType, stateCaptor.getValue().mimeType()); + assertEquals(file, fileCaptor.getValue()); + // Verify the state of ParseFile has been updated + assertEquals(url, parseFile.getUrl()); + } + + // TODO(grantland): testSaveAsyncNotDirtyAfterQueueAwait + // TODO(grantland): testSaveAsyncSuccess + // TODO(grantland): testSaveAsyncFailure + + //endregion + + + //region testGetDataAsync + + @Test + public void testGetDataAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + byte[] data = ParseTaskUtils.wait(parseFile.getDataInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), data); + + // Make sure we always get the data from network + byte[] dataAgain = ParseTaskUtils.wait(parseFile.getDataInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptorAgain = + ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(2)).fetchAsync( + stateCaptorAgain.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptorAgain.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), dataAgain); + } + + @Test + public void testGetDataStreamAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + InputStream dataStream = ParseTaskUtils.wait(parseFile.getDataStreamInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(dataStream)); + + // Make sure we always get the data from network + InputStream dataStreamAgain = ParseTaskUtils.wait(parseFile.getDataStreamInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptorAgain = + ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(2)).fetchAsync( + stateCaptorAgain.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptorAgain.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(dataStreamAgain)); + } + + @Test + public void testGetFileAsyncSuccess() throws Exception { + String content = "content"; + File file = temporaryFolder.newFile("test"); + ParseFileUtils.writeStringToFile(file, content, "UTF-8"); + ParseFileController controller = mock(ParseFileController.class); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(file)); + ParseCorePlugins.getInstance().registerFileController(controller); + + String url = "url"; + ParseFile.State state = new ParseFile.State.Builder() + .url(url) + .build(); + ParseFile parseFile = new ParseFile(state); + + File fetchedFile = ParseTaskUtils.wait(parseFile.getFileInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(1)).fetchAsync( + stateCaptor.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptor.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseFileUtils.readFileToByteArray(fetchedFile)); + + // Make sure we always get the data from network + File fetchedFileAgain = ParseTaskUtils.wait(parseFile.getFileInBackground()); + + // Verify controller get the correct data + ArgumentCaptor stateCaptorAgain = + ArgumentCaptor.forClass(ParseFile.State.class); + verify(controller, times(2)).fetchAsync( + stateCaptorAgain.capture(), + anyString(), + any(ProgressCallback.class), + Matchers.>any() + ); + assertEquals(url, stateCaptorAgain.getValue().url()); + // Verify the data we get is correct + assertArrayEquals(content.getBytes(), ParseFileUtils.readFileToByteArray(fetchedFileAgain)); + } + + //endregion + + @Test + public void testTaskQueuedMethods() throws Exception { + ParseFile.State state = new ParseFile.State.Builder().build(); + File cachedFile = temporaryFolder.newFile("temp"); + + ParseFileController controller = mock(ParseFileController.class); + when(controller.saveAsync( + any(ParseFile.State.class), + any(byte[].class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(state)); + when(controller.saveAsync( + any(ParseFile.State.class), + any(File.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(state)); + when(controller.fetchAsync( + any(ParseFile.State.class), + any(String.class), + any(ProgressCallback.class), + Matchers.>any())).thenReturn(Task.forResult(cachedFile)); + + ParseCorePlugins.getInstance().registerFileController(controller); + + ParseFile file = new ParseFile(state); + + TaskQueueTestHelper queueHelper = new TaskQueueTestHelper(file.taskQueue); + queueHelper.enqueue(); + + Task saveTaskA = file.saveAsync(null, null, null); + queueHelper.enqueue(); + Task getDataTaskA = file.getDataInBackground(); + queueHelper.enqueue(); + Task saveTaskB = file.saveAsync(null, null, null); + queueHelper.enqueue(); + Task getDataTaskB = file.getDataInBackground(); + + Thread.sleep(50); + assertFalse(saveTaskA.isCompleted()); + queueHelper.dequeue(); + ParseTaskUtils.wait(saveTaskA); + + Thread.sleep(50); + assertFalse(getDataTaskA.isCompleted()); + queueHelper.dequeue(); + ParseTaskUtils.wait(getDataTaskA); + + Thread.sleep(50); + assertFalse(saveTaskB.isCompleted()); + queueHelper.dequeue(); + ParseTaskUtils.wait(saveTaskB); + + Thread.sleep(50); + assertFalse(getDataTaskB.isCompleted()); + queueHelper.dequeue(); + ParseTaskUtils.wait(getDataTaskB); + } + + @Test + public void testCancel() { + ParseFile file = new ParseFile(new ParseFile.State.Builder().build()); + + TaskQueueTestHelper queueHelper = new TaskQueueTestHelper(file.taskQueue); + queueHelper.enqueue(); + + List> saveTasks = Arrays.asList( + file.saveInBackground(), + file.saveInBackground(), + file.saveInBackground()); + + List> getDataTasks = Arrays.asList( + file.getDataInBackground(), + file.getDataInBackground(), + file.getDataInBackground()); + + file.cancel(); + queueHelper.dequeue(); + + for (int i = 0; i < saveTasks.size(); i++ ) { + assertTrue("Task #" + i + " was not cancelled", saveTasks.get(i).isCancelled()); + } + for (int i = 0; i < getDataTasks.size(); i++ ) { + assertTrue("Task #" + i + " was not cancelled", getDataTasks.get(i).isCancelled()); + } + } + + @Test + public void testParcelable() { + String mime = "mime"; + String name = "name"; + String url = "url"; + ParseFile file = new ParseFile(new ParseFile.State.Builder() + .name(name) + .mimeType(mime) + .url(url) + .build()); + Parcel parcel = Parcel.obtain(); + file.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + file = ParseFile.CREATOR.createFromParcel(parcel); + assertEquals(file.getName(), name); + assertEquals(file.getUrl(), url); + assertEquals(file.getState().mimeType(), mime); + assertFalse(file.isDirty()); + } + + @Test( expected = RuntimeException.class ) + public void testDontParcelIfDirty() { + ParseFile file = new ParseFile(new ParseFile.State.Builder().build()); + Parcel parcel = Parcel.obtain(); + file.writeToParcel(parcel, 0); + } + + // TODO(grantland): testEncode + // TODO(grantland): testDecode +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileUtilsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileUtilsTest.java new file mode 100644 index 0000000..03d94ce --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseFileUtilsTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseFileUtilsTest { + + private static final String TEST_STRING = "this is a test string"; + private static final String TEST_JSON = "{ \"foo\": \"bar\" }"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testReadFileToString() throws Exception { + File file = temporaryFolder.newFile("file.txt"); + BufferedOutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + out.write(TEST_STRING.getBytes("UTF-8")); + } finally { + ParseIOUtils.closeQuietly(out); + } + + assertEquals(TEST_STRING, ParseFileUtils.readFileToString(file, "UTF-8")); + } + + @Test + public void testWriteStringToFile() throws Exception { + File file = temporaryFolder.newFile("file.txt"); + ParseFileUtils.writeStringToFile(file, TEST_STRING, "UTF-8"); + + InputStream in = null; + String content = null; + try { + in = new FileInputStream(file); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ParseIOUtils.copy(in, out); + content = new String(out.toByteArray(), "UTF-8"); + } finally { + ParseIOUtils.closeQuietly(in); + } + + assertEquals(TEST_STRING, content); + } + + @Test + public void testReadFileToJSONObject() throws Exception { + File file = temporaryFolder.newFile("file.txt"); + BufferedOutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + out.write(TEST_JSON.getBytes("UTF-8")); + } finally { + ParseIOUtils.closeQuietly(out); + } + + JSONObject json = ParseFileUtils.readFileToJSONObject(file); + assertNotNull(json); + assertEquals("bar", json.getString("foo")); + } + + @Test + public void testWriteJSONObjectToFile() throws Exception { + File file = temporaryFolder.newFile("file.txt"); + ParseFileUtils.writeJSONObjectToFile(file, new JSONObject(TEST_JSON)); + + InputStream in = null; + String content = null; + try { + in = new FileInputStream(file); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ParseIOUtils.copy(in, out); + content = new String(out.toByteArray()); + } finally { + ParseIOUtils.closeQuietly(in); + } + + JSONObject json = new JSONObject(content); + assertNotNull(json); + assertEquals("bar", json.getString("foo")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseGeoPointTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseGeoPointTest.java new file mode 100644 index 0000000..dea3a37 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseGeoPointTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseGeoPointTest { + + @Test + public void testConstructors() { + ParseGeoPoint point = new ParseGeoPoint(); + assertEquals(0, point.getLatitude(), 0); + assertEquals(0, point.getLongitude(), 0); + + double lat = 1.0; + double lng = 2.0; + point = new ParseGeoPoint(lat, lng); + assertEquals(lat, point.getLatitude(), 0); + assertEquals(lng, point.getLongitude(), 0); + + ParseGeoPoint copy = new ParseGeoPoint(point); + assertEquals(lat, copy.getLatitude(), 0); + assertEquals(lng, copy.getLongitude(), 0); + } + + @Test + public void testEquals() { + ParseGeoPoint pointA = new ParseGeoPoint(30d, 50d); + ParseGeoPoint pointB = new ParseGeoPoint(30d, 50d); + ParseGeoPoint pointC = new ParseGeoPoint(45d, 45d); + + assertTrue(pointA.equals(pointB)); + assertTrue(pointA.equals(pointA)); + assertTrue(pointB.equals(pointA)); + + assertFalse(pointA.equals(null)); + assertFalse(pointA.equals(true)); + assertFalse(pointA.equals(pointC)); + } + + @Test + public void testParcelable() { + ParseGeoPoint point = new ParseGeoPoint(30d, 50d); + Parcel parcel = Parcel.obtain(); + point.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + point = ParseGeoPoint.CREATOR.createFromParcel(parcel); + assertEquals(point.getLatitude(), 30d, 0); + assertEquals(point.getLongitude(), 50d, 0); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpClientTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpClientTest.java new file mode 100644 index 0000000..f17afda --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpClientTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.zip.GZIPOutputStream; + +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseHttpClientTest { + + // We can not use ParameterizedRobolectricTestRunner right now since Robolectric use + // default java classloader when we construct the parameters. However + // SSLCertificateSocketFactory is only mocked under Robolectric classloader. + + @Test + public void testParseOkHttpClientExecuteWithSuccessResponse() throws Exception { + doSingleParseHttpClientExecuteWithResponse( + 200, "OK", "Success", ParseHttpClient.createClient(new OkHttpClient.Builder())); } + + @Test + public void testParseOkHttpClientExecuteWithErrorResponse() throws Exception { + doSingleParseHttpClientExecuteWithResponse( + 404, "NOT FOUND", "Error", ParseHttpClient.createClient(new OkHttpClient.Builder())); } + + // TODO(mengyan): Add testParseURLConnectionHttpClientExecuteWithGzipResponse, right now we can + // not do that since in unit test env, URLConnection does not use OKHttp internally, so there is + // no transparent ungzip + + @Test + public void testParseOkHttpClientExecuteWithGzipResponse() throws Exception { + doSingleParseHttpClientExecuteWithGzipResponse( + 200, "OK", "Success", ParseHttpClient.createClient(new OkHttpClient.Builder())); + } + + private void doSingleParseHttpClientExecuteWithResponse(int responseCode, String responseStatus, + String responseContent, ParseHttpClient client) throws Exception { + MockWebServer server = new MockWebServer(); + + // Make mock response + int responseContentLength = responseContent.length(); + MockResponse mockResponse = new MockResponse() + .setStatus("HTTP/1.1 " + responseCode + " " + responseStatus) + .setBody(responseContent); + + // Start mock server + server.enqueue(mockResponse); + server.start(); + + // Make ParseHttpRequest + Map requestHeaders = new HashMap<>(); + requestHeaders.put("User-Agent", "Parse Android SDK"); + + String requestUrl = server.url("/").toString(); + JSONObject json = new JSONObject(); + json.put("key", "value"); + String requestContent = json.toString(); + int requestContentLength = requestContent.length(); + String requestContentType = "application/json"; + ParseHttpRequest parseRequest = new ParseHttpRequest.Builder() + .setUrl(requestUrl) + .setMethod(ParseHttpRequest.Method.POST) + .setBody(new ParseByteArrayHttpBody(requestContent, requestContentType)) + .setHeaders(requestHeaders) + .build(); + + // Execute request + ParseHttpResponse parseResponse = client.execute(parseRequest); + + RecordedRequest recordedApacheRequest = server.takeRequest(); + + // Verify request method + assertEquals(ParseHttpRequest.Method.POST.toString(), recordedApacheRequest.getMethod()); + + // Verify request headers, since http library automatically adds some headers, we only need to + // verify all parseRequest headers are in recordedRequest headers. + Headers recordedApacheHeaders = recordedApacheRequest.getHeaders(); + Set recordedApacheHeadersNames = recordedApacheHeaders.names(); + for (String name : parseRequest.getAllHeaders().keySet()) { + assertTrue(recordedApacheHeadersNames.contains(name)); + assertEquals(parseRequest.getAllHeaders().get(name), recordedApacheHeaders.get(name)); + } + + // Verify request body + assertEquals(requestContentLength, recordedApacheRequest.getBodySize()); + assertArrayEquals(requestContent.getBytes(), recordedApacheRequest.getBody().readByteArray()); + + // Verify response status code + assertEquals(responseCode, parseResponse.getStatusCode()); + // Verify response status + assertEquals(responseStatus, parseResponse.getReasonPhrase()); + // Verify all response header entries' keys and values are not null. + for (Map.Entry entry : parseResponse.getAllHeaders().entrySet()) { + assertNotNull(entry.getKey()); + assertNotNull(entry.getValue()); + } + // Verify response body + byte[] content = ParseIOUtils.toByteArray(parseResponse.getContent()); + assertArrayEquals(responseContent.getBytes(), content); + // Verify response body size + assertEquals(responseContentLength, content.length); + + // Shutdown mock server + server.shutdown(); + } + + private void doSingleParseHttpClientExecuteWithGzipResponse( + int responseCode, String responseStatus, final String responseContent, ParseHttpClient client) + throws Exception { + MockWebServer server = new MockWebServer(); + + // Make mock response + Buffer buffer = new Buffer(); + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + GZIPOutputStream gzipOut = new GZIPOutputStream(byteOut); + gzipOut.write(responseContent.getBytes()); + gzipOut.close(); + buffer.write(byteOut.toByteArray()); + MockResponse mockResponse = new MockResponse() + .setStatus("HTTP/1.1 " + responseCode + " " + responseStatus) + .setBody(buffer) + .setHeader("Content-Encoding", "gzip"); + + // Start mock server + server.enqueue(mockResponse); + server.start(); + + // We do not need to add Accept-Encoding header manually, httpClient library should do that. + String requestUrl = server.url("/").toString(); + ParseHttpRequest parseRequest = new ParseHttpRequest.Builder() + .setUrl(requestUrl) + .setMethod(ParseHttpRequest.Method.GET) + .build(); + + // Execute request + ParseHttpResponse parseResponse = client.execute(parseRequest); + + RecordedRequest recordedRequest = server.takeRequest(); + + // Verify request method + assertEquals(ParseHttpRequest.Method.GET.toString(), recordedRequest.getMethod()); + + // Verify request headers + Headers recordedHeaders = recordedRequest.getHeaders(); + + assertEquals("gzip", recordedHeaders.get("Accept-Encoding")); + + // Verify we do not have Content-Encoding header + assertNull(parseResponse.getHeader("Content-Encoding")); + + // Verify response body + byte[] content = ParseIOUtils.toByteArray(parseResponse.getContent()); + assertArrayEquals(responseContent.getBytes(), content); + + // Shutdown mock server + server.shutdown(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpRequestTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpRequestTest.java new file mode 100644 index 0000000..6f1adae --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpRequestTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; + +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ParseHttpRequestTest { + + @Test + public void testParseHttpRequestGetMethod() throws IOException { + String url = "www.parse.com"; + ParseHttpRequest.Method method = ParseHttpRequest.Method.POST; + Map headers = new HashMap<>(); + String name = "name"; + String value = "value"; + headers.put(name, value); + + String content = "content"; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + + ParseHttpRequest request = new ParseHttpRequest.Builder() + .setUrl(url) + .addHeader(name, value) + .setMethod(method) + .setBody(body) + .build(); + + assertEquals(url, request.getUrl()); + assertEquals(method.toString(), request.getMethod().toString()); + assertEquals(1, request.getAllHeaders().size()); + assertEquals(value, request.getHeader(name)); + ParseHttpBody bodyAgain = request.getBody(); + assertEquals(contentType, bodyAgain.getContentType()); + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(body.getContent())); + } + + @Test + public void testParseHttpRequestBuilderInitialization() throws IOException { + String url = "www.parse.com"; + ParseHttpRequest.Method method = ParseHttpRequest.Method.POST; + Map headers = new HashMap<>(); + String name = "name"; + String value = "value"; + headers.put(name, value); + + String content = "content"; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + + ParseHttpRequest request = new ParseHttpRequest.Builder() + .setUrl(url) + .addHeader(name, value) + .setMethod(method) + .setBody(body) + .build(); + + ParseHttpRequest requestAgain = new ParseHttpRequest.Builder(request).build(); + + assertEquals(url, requestAgain.getUrl()); + assertEquals(method.toString(), requestAgain.getMethod().toString()); + assertEquals(1, requestAgain.getAllHeaders().size()); + assertEquals(value, requestAgain.getHeader(name)); + ParseHttpBody bodyAgain = requestAgain.getBody(); + assertEquals(contentType, bodyAgain.getContentType()); + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(body.getContent())); + } + + @Test + public void testParseHttpRequestBuildWithParseHttpRequest() throws IOException { + String url = "www.parse.com"; + ParseHttpRequest.Method method = ParseHttpRequest.Method.POST; + Map headers = new HashMap<>(); + String name = "name"; + String value = "value"; + headers.put(name, value); + + String content = "content"; + String contentType = "application/json"; + ParseByteArrayHttpBody body = new ParseByteArrayHttpBody(content, contentType); + + ParseHttpRequest request = new ParseHttpRequest.Builder() + .setUrl(url) + .addHeader(name, value) + .setMethod(method) + .setBody(body) + .build(); + + String newURL = "www.api.parse.com"; + ParseHttpRequest newRequest = new ParseHttpRequest.Builder(request) + .setUrl(newURL) + .build(); + + assertEquals(newURL, newRequest.getUrl()); + assertEquals(method.toString(), newRequest.getMethod().toString()); + assertEquals(1, newRequest.getAllHeaders().size()); + assertEquals(value, newRequest.getHeader(name)); + ParseHttpBody bodyAgain = newRequest.getBody(); + assertEquals(contentType, bodyAgain.getContentType()); + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(body.getContent())); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpResponseTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpResponseTest.java new file mode 100644 index 0000000..6380d3d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseHttpResponseTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpResponse; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class ParseHttpResponseTest { + + @Test + public void testParseHttpResponseDefaults() throws IOException { + ParseHttpResponse response = new ParseHttpResponse.Builder().build(); + + assertNull(response.getContent()); + assertNull(response.getContentType()); + assertNull(response.getReasonPhrase()); + assertEquals(0, response.getStatusCode()); + assertEquals(-1, response.getTotalSize()); + assertEquals(0, response.getAllHeaders().size()); + assertNull(response.getHeader("test")); + } + + @Test + public void testParseHttpResponseGetMethod() throws IOException { + Map headers = new HashMap<>(); + String name = "name"; + String value = "value"; + headers.put(name, value); + String content = "content"; + String contentType = "application/json"; + String reasonPhrase = "OK"; + int statusCode = 200; + int totalSize = content.length(); + + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(content.getBytes())) + .setContentType(contentType) + .setHeaders(headers) + .setReasonPhrase(reasonPhrase) + .setStatusCode(statusCode) + .setTotalSize(totalSize) + .build(); + + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(response.getContent())); + assertEquals(contentType, response.getContentType()); + assertEquals(reasonPhrase, response.getReasonPhrase()); + assertEquals(statusCode, response.getStatusCode()); + assertEquals(totalSize, response.getTotalSize()); + assertEquals(value, response.getHeader(name)); + assertEquals(1, response.getAllHeaders().size()); + } + + @Test + public void testParseHttpResponseBuildWithParseHttpResponse() throws IOException { + Map headers = new HashMap<>(); + String name = "name"; + String value = "value"; + headers.put(name, value); + String content = "content"; + String contentType = "application/json"; + String reasonPhrase = "OK"; + int statusCode = 200; + int totalSize = content.length(); + + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(content.getBytes())) + .setContentType(contentType) + .setHeaders(headers) + .setReasonPhrase(reasonPhrase) + .setStatusCode(statusCode) + .setTotalSize(totalSize) + .build(); + + String newReasonPhrase = "Failed"; + ParseHttpResponse newResponse = new ParseHttpResponse.Builder(response) + .setReasonPhrase(newReasonPhrase) + .build(); + + assertEquals(contentType, newResponse.getContentType()); + assertEquals(newReasonPhrase, newResponse.getReasonPhrase()); + assertEquals(statusCode, newResponse.getStatusCode()); + assertEquals(totalSize, newResponse.getTotalSize()); + assertEquals(value, newResponse.getHeader(name)); + assertEquals(1, newResponse.getAllHeaders().size()); + assertSame(response.getContent(), newResponse.getContent()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseImpreciseDateFormatTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseImpreciseDateFormatTest.java new file mode 100644 index 0000000..681e2ac --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseImpreciseDateFormatTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ParseImpreciseDateFormatTest { + @Test + public void testParse() { + String string = "2015-05-13T11:08:01Z"; + Date date = ParseImpreciseDateFormat.getInstance().parse(string); + assertEquals(1431515281000L, date.getTime()); + } + + @Test + public void testParseInvalid() { + String string = "2015-05-13T11:08:01.123Z"; + Date date = ParseImpreciseDateFormat.getInstance().parse(string); + assertNull(date); + } + + @Test + public void testFormat() { + Date date = new Date(1431515281000L); + String string = ParseImpreciseDateFormat.getInstance().format(date); + assertEquals("2015-05-13T11:08:01Z", string); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseInstallationTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseInstallationTest.java new file mode 100644 index 0000000..a5617f0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseInstallationTest.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Locale; +import java.util.TimeZone; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseInstallationTest extends ResetPluginsParseTest { + private static final String KEY_INSTALLATION_ID = "installationId"; + private static final String KEY_DEVICE_TYPE = "deviceType"; + private static final String KEY_APP_NAME = "appName"; + private static final String KEY_APP_IDENTIFIER = "appIdentifier"; + private static final String KEY_TIME_ZONE = "timeZone"; + private static final String KEY_LOCALE_IDENTIFIER = "localeIdentifier"; + private static final String KEY_APP_VERSION = "appVersion"; + + private Locale defaultLocale; + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseObject.registerSubclass(ParseInstallation.class); + + defaultLocale = Locale.getDefault(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + ParseObject.unregisterSubclass(ParseInstallation.class); + + Locale.setDefault(defaultLocale); + } + + @Test + public void testImmutableKeys() { + String[] immutableKeys = { + "installationId", + "deviceType", + "appName", + "appIdentifier", + "parseVersion", + "deviceToken", + "deviceTokenLastModified", + "pushType", + "timeZone", + "localeIdentifier", + "appVersion" + }; + + ParseInstallation installation = new ParseInstallation(); + installation.put("foo", "bar"); + + for (String immutableKey : immutableKeys) { + try { + installation.put(immutableKey, "blah"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + installation.remove(immutableKey); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + installation.removeAll(immutableKey, Arrays.asList()); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + } + } + + @Test (expected = RuntimeException.class) + public void testInstallationObjectIdCannotBeChanged() throws Exception { + boolean hasException = false; + ParseInstallation installation = new ParseInstallation(); + try { + installation.put("objectId", "abc"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + hasException = true; + } + assertTrue(hasException); + installation.setObjectId("abc"); + } + + @Test + public void testMissingRequiredFieldWhenSaveAsync() throws Exception { + String sessionToken = "sessionToken"; + Task toAwait = Task.forResult(null); + + ParseCurrentInstallationController controller = mockCurrentInstallationController(); + + ParseObjectController objController = mock(ParseObjectController.class); + // mock return task when Installation was deleted on the server + Task taskError = Task.forError(new ParseException(ParseException.MISSING_REQUIRED_FIELD_ERROR, "")); + // mock return task when Installation was re-saved to the server + Task task = Task.forResult(null); + when(objController.saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class))) + .thenReturn(taskError) + .thenReturn(task); + ParseCorePlugins.getInstance() + .registerObjectController(objController); + + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + assertNotNull(installation); + installation.put("key", "value"); + installation.saveAsync(sessionToken, toAwait); + verify(controller).getAsync(); + verify(objController, times(2)).saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class)); + } + + @Test + public void testObjectNotFoundWhenSaveAsync() throws Exception { + OfflineStore lds = new OfflineStore(RuntimeEnvironment.application); + Parse.setLocalDatastore(lds); + + String sessionToken = "sessionToken"; + Task toAwait = Task.forResult(null); + + ParseCurrentInstallationController controller = mockCurrentInstallationController(); + ParseObjectController objController = mock(ParseObjectController.class); + // mock return task when Installation was deleted on the server + Task taskError = Task.forError(new ParseException(ParseException.OBJECT_NOT_FOUND, "")); + // mock return task when Installation was re-saved to the server + Task task = Task.forResult(null); + when(objController.saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class))) + .thenReturn(taskError) + .thenReturn(task); + ParseCorePlugins.getInstance() + .registerObjectController(objController); + + ParseObject.State state = new ParseObject.State.Builder("_Installation") + .objectId("oldId") + .put("deviceToken", "deviceToken") + .build(); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + assertNotNull(installation); + installation.setState(state); + installation.put("key", "value"); + installation.saveAsync(sessionToken, toAwait); + + verify(controller).getAsync(); + verify(objController, times(2)).saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class)); + Parse.setLocalDatastore(null); + } + + @Test + public void testHandleSaveResultAsync() throws Exception { + // Mock currentInstallationController to make setAsync work + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + when(controller.setAsync(any(ParseInstallation.class))).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); + // Mock return state + ParseInstallation.State state = new ParseInstallation.State.Builder("_Installation") + .put("key", "value") + .build(); + + ParseInstallation installation = new ParseInstallation(); + installation.put("keyAgain", "valueAgain"); + ParseOperationSet operationSet = installation.startSave(); + ParseTaskUtils.wait(installation.handleSaveResultAsync(state, operationSet)); + + // Make sure the installation data is correct + assertEquals("value", installation.get("key")); + assertEquals("valueAgain", installation.get("keyAgain")); + // Make sure we set the currentInstallation + verify(controller, times(1)).setAsync(installation); + } + + @Test + public void testHandleFetchResultAsync() throws Exception { + // Mock currentInstallationController to make setAsync work + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + when(controller.setAsync(any(ParseInstallation.class))).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); + // Mock return state + ParseInstallation.State state = new ParseInstallation.State.Builder("_Installation") + .put("key", "value") + .isComplete(true) + .build(); + + ParseInstallation installation = new ParseInstallation(); + ParseTaskUtils.wait(installation.handleFetchResultAsync(state)); + + // Make sure the installation data is correct + assertEquals("value", installation.get("key")); + // Make sure we set the currentInstallation + verify(controller, times(1)).setAsync(installation); + } + + @Test + public void testUpdateBeforeSave() throws Exception { + mocksForUpdateBeforeSave(); + + Locale.setDefault(new Locale("en", "US")); + + ParseInstallation installation = new ParseInstallation(); + installation.updateBeforeSave(); + + // Make sure we update timezone + String zone = installation.getString(KEY_TIME_ZONE); + String deviceZone = TimeZone.getDefault().getID(); + if (zone != null) { + assertEquals(zone, deviceZone); + } else { + // If it's not updated it's because it was not acceptable. + assertFalse(deviceZone.equals("GMT")); + assertFalse(deviceZone.indexOf("/") > 0); + } + + // Make sure we update version info + Context context = Parse.getApplicationContext(); + String packageName = context.getPackageName(); + PackageManager pm = context.getPackageManager(); + PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); + String appVersion = pkgInfo.versionName; + String appName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString(); + assertEquals(packageName, installation.getString(KEY_APP_IDENTIFIER)); + assertEquals(appName, installation.getString(KEY_APP_NAME)); + assertEquals(appVersion, installation.getString(KEY_APP_VERSION)); + + // Make sure we update device info + assertEquals("android", installation.getString(KEY_DEVICE_TYPE)); + assertEquals("installationId", installation.getString(KEY_INSTALLATION_ID)); + + // Make sure we update the locale identifier + assertEquals("en-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + } + + // TODO(mengyan): Add other testUpdateBeforeSave cases to cover all branches + + @Test + public void testPushType() throws Exception { + ParseInstallation installation = new ParseInstallation(); + installation.setPushType(PushType.GCM); + + assertEquals(PushType.GCM, installation.getPushType()); + + installation.removePushType(); + + assertNull(installation.getPushType()); + // Make sure we add the pushType to operationSetQueue instead of serverData + assertEquals(1, installation.operationSetQueue.getLast().size()); + } + + @Test + public void testPushTypeWithNullPushType() throws Exception { + ParseInstallation installation = new ParseInstallation(); + installation.setPushType(PushType.GCM); + + assertEquals(PushType.GCM, installation.getPushType()); + + installation.setPushType(null); + + assertEquals(PushType.GCM, installation.getPushType()); + } + + @Test + public void testDeviceToken() throws Exception { + ParseInstallation installation = new ParseInstallation(); + installation.setDeviceToken("deviceToken"); + + assertEquals("deviceToken", installation.getDeviceToken()); + + installation.removeDeviceToken(); + + assertNull(installation.getDeviceToken()); + // Make sure we add the pushType to operationSetQueue instead of serverData + assertEquals(1, installation.operationSetQueue.getLast().size()); + } + + @Test + public void testDeviceTokenWithNullDeviceToken() throws Exception { + ParseInstallation installation = new ParseInstallation(); + installation.setDeviceToken("deviceToken"); + + assertEquals("deviceToken", installation.getDeviceToken()); + + installation.setDeviceToken(null); + + assertEquals("deviceToken", installation.getDeviceToken()); + } + + @Test + public void testGetCurrentInstallation() throws Exception { + // Mock currentInstallationController to make setAsync work + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + ParseInstallation currentInstallation = new ParseInstallation(); + when(controller.getAsync()).thenReturn(Task.forResult(currentInstallation)); + ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); + + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + + assertEquals(currentInstallation, installation); + verify(controller, times(1)).getAsync(); + } + + @Test + public void testLocaleIdentifierSpecialCases() throws Exception { + mocksForUpdateBeforeSave(); + + ParseInstallation installation = new ParseInstallation(); + + // Deprecated two-letter codes (Java issue). + Locale.setDefault(new Locale("iw", "US")); + installation.updateBeforeSave(); + assertEquals("he-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + Locale.setDefault(new Locale("in", "US")); + installation.updateBeforeSave(); + assertEquals("id-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + Locale.setDefault(new Locale("ji", "US")); + installation.updateBeforeSave(); + assertEquals("yi-US", installation.getString(KEY_LOCALE_IDENTIFIER)); + + // No country code. + Locale.setDefault(new Locale("en")); + installation.updateBeforeSave(); + assertEquals("en", installation.getString(KEY_LOCALE_IDENTIFIER)); + } + + + + // TODO(mengyan): Add testFetchAsync, right now we can not test super methods inside + // testFetchAsync + + private static void mocksForUpdateBeforeSave() { + // Mock currentInstallationController to make setAsync work + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + when(controller.isCurrent(any(ParseInstallation.class))).thenReturn(true); + ParseCorePlugins.getInstance().registerCurrentInstallationController(controller); + // Mock App Name + RuntimeEnvironment.application.getApplicationInfo().name = "parseTest"; + ParsePlugins plugins = mock(ParsePlugins.class); + // Mock installationId + InstallationId installationId = mock(InstallationId.class); + when(installationId.get()).thenReturn("installationId"); + when(plugins.installationId()).thenReturn(installationId); + // Mock application context + when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); + ParsePlugins.set(plugins); + } + + private ParseCurrentInstallationController mockCurrentInstallationController() { + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + ParseInstallation currentInstallation = new ParseInstallation(); + when(controller.getAsync()) + .thenReturn(Task.forResult(currentInstallation)); + ParseCorePlugins.getInstance() + .registerCurrentInstallationController(controller); + return controller; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseKeyValueCacheTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseKeyValueCacheTest.java new file mode 100644 index 0000000..ac55d8e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseKeyValueCacheTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import bolts.Task; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ParseKeyValueCacheTest { + + private File keyValueCacheDir; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + keyValueCacheDir = temporaryFolder.newFolder("ParseKeyValueCache"); + ParseKeyValueCache.initialize(keyValueCacheDir); + } + + @After + public void tearDown() throws Exception { + ParseKeyValueCache.clearKeyValueCacheDir(); + ParseKeyValueCache.maxKeyValueCacheBytes = ParseKeyValueCache.DEFAULT_MAX_KEY_VALUE_CACHE_BYTES; + ParseKeyValueCache.maxKeyValueCacheFiles = ParseKeyValueCache.DEFAULT_MAX_KEY_VALUE_CACHE_FILES; + } + + @Test + public void testMultipleAsynchronousWrites() throws ParseException { + int max = 100; + ParseKeyValueCache.maxKeyValueCacheFiles = max; + + // Max out KeyValueCache + for (int i = 0; i < max; i++) { + ParseKeyValueCache.saveToKeyValueCache("key " + i, "test"); + } + + List> tasks = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + tasks.add(Task.call(new Callable() { + @Override + public Void call() throws Exception { + ParseKeyValueCache.saveToKeyValueCache("foo", "test"); + return null; + } + }, Task.BACKGROUND_EXECUTOR)); + } + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + + @Test + public void testSaveToKeyValueCacheWithoutCacheDir() throws Exception { + // Delete the cache folder(Simulate users clear the app cache) + assertTrue(keyValueCacheDir.exists()); + keyValueCacheDir.delete(); + assertFalse(keyValueCacheDir.exists()); + + // Save a key value pair + ParseKeyValueCache.saveToKeyValueCache("key", "value"); + + // Verify cache file is correct + assertEquals(1, keyValueCacheDir.listFiles().length); + assertArrayEquals( + "value".getBytes(), ParseFileUtils.readFileToByteArray(keyValueCacheDir.listFiles()[0])); + } + + @Test + public void testGetSizeWithoutCacheDir() throws Exception { + // Delete the cache folder(Simulate users clear the app cache) + assertTrue(keyValueCacheDir.exists()); + keyValueCacheDir.delete(); + assertFalse(keyValueCacheDir.exists()); + + // Verify size is zero + assertEquals(0, ParseKeyValueCache.size()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseMatchers.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseMatchers.java new file mode 100644 index 0000000..73df255 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseMatchers.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.internal.matchers.TypeSafeMatcher; + +import static org.hamcrest.CoreMatchers.equalTo; + +public class ParseMatchers { + public static Matcher hasParseErrorCode(int code) { + return hasParseErrorCode(equalTo(code)); + } + + public static Matcher hasParseErrorCode(final Matcher matcher) { + return new TypeSafeMatcher() { + @Override + public boolean matchesSafely(Throwable item) { + return item instanceof ParseException + && matcher.matches(((ParseException) item).getCode()); + } + + @Override + public void describeTo(Description description) { + description.appendText("exception with message "); + description.appendDescriptionOf(matcher); + } + }; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectCurrentCoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectCurrentCoderTest.java new file mode 100644 index 0000000..c1850d8 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectCurrentCoderTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.SimpleTimeZone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class ParseObjectCurrentCoderTest { + + // These magic strings are copied from ParseObjectCurrentCoder, since we do not want to make + // the magic strings in ParseObjectCurrentCoder to be package for tests. + /* + /2 format JSON Keys + */ + private static final String KEY_OBJECT_ID = "objectId"; + private static final String KEY_CLASS_NAME = "classname"; + private static final String KEY_CREATED_AT = "createdAt"; + private static final String KEY_UPDATED_AT = "updatedAt"; + private static final String KEY_DATA = "data"; + + /* + Old serialized JSON keys + */ + private static final String KEY_OLD_OBJECT_ID = "id"; + private static final String KEY_OLD_CREATED_AT = "created_at"; + private static final String KEY_OLD_UPDATED_AT = "updated_at"; + private static final String KEY_OLD_POINTERS = "pointers"; + + + @Test + public void testEncodeSuccess() throws Exception { + Date createAt = new Date(1000); + Date updateAt = new Date(2000); + ParseObject.State state = new ParseObject.State.Builder("Test") + .createdAt(createAt) + .updatedAt(updateAt) + .objectId("objectId") + .put("key", "value") + .build(); + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + JSONObject objectJson = coder.encode(state, null, PointerEncoder.get()); + + assertEquals("Test", objectJson.getString(KEY_CLASS_NAME)); + JSONObject dataJson = objectJson.getJSONObject(KEY_DATA); + String createAtStr = ParseDateFormat.getInstance().format(createAt); + assertEquals(createAtStr, dataJson.getString(KEY_CREATED_AT)); + String updateAtStr = ParseDateFormat.getInstance().format(updateAt); + assertEquals(updateAtStr, dataJson.getString(KEY_UPDATED_AT)); + assertEquals("objectId", dataJson.getString(KEY_OBJECT_ID)); + assertEquals("value", dataJson.getString("key")); + } + + @Test + public void testEncodeSuccessWithEmptyState() throws Exception { + ParseObject.State state = new ParseObject.State.Builder("Test") + .build(); + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + JSONObject objectJson = coder.encode(state, null, PointerEncoder.get()); + + assertEquals("Test", objectJson.getString(KEY_CLASS_NAME)); + JSONObject dataJson = objectJson.getJSONObject(KEY_DATA); + assertFalse(dataJson.has(KEY_CREATED_AT)); + assertFalse(dataJson.has(KEY_UPDATED_AT)); + assertFalse(dataJson.has(KEY_OBJECT_ID)); + } + + @Test(expected = IllegalArgumentException.class) + public void testEncodeFailureWithNotNullParseOperationSet() throws Exception { + ParseObject.State state = new ParseObject.State.Builder("Test") + .build(); + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + coder.encode(state, new ParseOperationSet(), PointerEncoder.get()); + } + + @Test + public void testDecodeSuccessWithoutOldFormatJson() throws Exception { + Date createAt = new Date(1000); + Date updateAt = new Date(2000); + String createAtStr = ParseDateFormat.getInstance().format(createAt); + String updateAtStr = ParseDateFormat.getInstance().format(updateAt); + JSONObject dataJson = new JSONObject() + .put(KEY_OBJECT_ID, "objectId") + .put(KEY_CREATED_AT, createAtStr) + .put(KEY_UPDATED_AT, updateAtStr) + .put("key", "value"); + JSONObject objectJson = new JSONObject(); + objectJson.put(KEY_DATA, dataJson); + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + ParseObject.State.Builder builder = + coder.decode(new ParseObject.State.Builder("Test"), objectJson, ParseDecoder.get()); + + // We use the builder to build a state to verify the content in the builder + ParseObject.State state = builder.build(); + assertEquals(createAt.getTime(), state.createdAt()); + assertEquals(updateAt.getTime(), state.updatedAt()); + assertEquals("objectId", state.objectId()); + assertEquals("value", state.get("key")); + } + + @Test + public void testDecodeSuccessWithOldFormatJson() throws Exception { + Date createAt = new Date(1000); + Date updateAt = new Date(2000); + String createAtStr = ParseImpreciseDateFormat.getInstance().format(createAt); + String updateAtStr = ParseImpreciseDateFormat.getInstance().format(updateAt); + JSONObject pointerJson = new JSONObject(); + JSONArray innerObjectJson = new JSONArray() + .put(0, "innerObject") + .put(1, "innerObjectId"); + pointerJson.put("inner", innerObjectJson); + JSONObject oldObjectJson = new JSONObject() + .put(KEY_OLD_OBJECT_ID, "objectId") + .put(KEY_OLD_CREATED_AT, createAtStr) + .put(KEY_OLD_UPDATED_AT, updateAtStr) + .put(KEY_OLD_POINTERS, pointerJson); + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + ParseObject.State.Builder builder = + coder.decode(new ParseObject.State.Builder("Test"), oldObjectJson, ParseDecoder.get()); + + // We use the builder to build a state to verify the content in the builder + ParseObject.State state = builder.build(); + assertEquals(createAt.getTime(), state.createdAt()); + assertEquals(updateAt.getTime(), state.updatedAt()); + assertEquals("objectId", state.objectId()); + ParseObject innerObject = (ParseObject) state.get("inner"); + assertEquals("innerObject", innerObject.getClassName()); + assertEquals("innerObjectId", innerObject.getObjectId()); + } + + @Test + public void testObjectSerializationFormat() throws Exception { + ParseObject childObject = new ParseObject("child"); + childObject.setObjectId("childObjectId"); + + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + format.setTimeZone(new SimpleTimeZone(0, "GMT")); + + String dateString = "2011-08-12T01:06:05Z"; + Date date = format.parse(dateString); + + String jsonString = "{" + + "'id':'wnAiJVI3ra'," + + "'updated_at':'" + dateString + "'," + + "'pointers':{'child':['child','" + childObject.getObjectId() + "']}," + + "'classname':'myClass'," + + "'dirty':true," + + "'data':{'foo':'bar'}," + + "'created_at':'2011-08-12T01:06:05Z'," + + "'deletedKeys':['toDelete']" + + "}"; + + ParseObjectCurrentCoder coder = ParseObjectCurrentCoder.get(); + JSONObject json = new JSONObject(jsonString); + ParseObject.State state = coder.decode( + new ParseObject.State.Builder("Test"), json, ParseDecoder.get()).build(); + + assertEquals("wnAiJVI3ra", state.objectId()); + assertEquals("bar", state.get("foo")); + assertEquals(date.getTime(), state.createdAt()); + assertEquals(((ParseObject) state.get("child")).getObjectId(), childObject.getObjectId()); + + // Test that objects can be serialized and deserialized without timestamps + String jsonStringWithoutTimestamps = "{" + + "'id':'wnAiJVI3ra'," + + "'pointers':{'child':['child','" + childObject.getObjectId() + "']}," + + "'classname':'myClass'," + + "'dirty':true," + + "'data':{'foo':'bar'}," + + "'deletedKeys':['toDelete']" + + "}"; + + json = new JSONObject(jsonStringWithoutTimestamps); + state = coder.decode( + new ParseObject.State.Builder("Test"), json, ParseDecoder.get()).build(); + assertEquals("wnAiJVI3ra", state.objectId()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectStateTest.java new file mode 100644 index 0000000..76b88b4 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseObjectStateTest { + + @Test + public void testDefaults() { + ParseObject.State state = new ParseObject.State.Builder("TestObject").build(); + assertEquals("TestObject", state.className()); + assertNull(state.objectId()); + assertEquals(-1, state.createdAt()); + assertEquals(-1, state.updatedAt()); + assertFalse(state.isComplete()); + assertTrue(state.keySet().isEmpty()); + assertTrue(state.availableKeys().isEmpty()); + } + + @Test + public void testProperties() { + long updatedAt = System.currentTimeMillis(); + long createdAt = updatedAt + 10; + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .objectId("fake") + .createdAt(new Date(createdAt)) + .updatedAt(new Date(updatedAt)) + .isComplete(true) + .build(); + assertEquals("TestObject", state.className()); + assertEquals("fake", state.objectId()); + assertEquals(createdAt, state.createdAt()); + assertEquals(updatedAt, state.updatedAt()); + assertTrue(state.isComplete()); + } + + @Test + public void testCopy() { + long updatedAt = System.currentTimeMillis(); + long createdAt = updatedAt + 10; + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .objectId("fake") + .createdAt(new Date(createdAt)) + .updatedAt(new Date(updatedAt)) + .isComplete(true) + .put("foo", "bar") + .put("baz", "qux") + .availableKeys(Arrays.asList("safe", "keys")) + .build(); + ParseObject.State copy = new ParseObject.State.Builder(state).build(); + assertEquals(state.className(), copy.className()); + assertEquals(state.objectId(), copy.objectId()); + assertEquals(state.createdAt(), copy.createdAt()); + assertEquals(state.updatedAt(), copy.updatedAt()); + assertEquals(state.isComplete(), copy.isComplete()); + assertEquals(state.keySet().size(), copy.keySet().size()); + assertEquals(state.get("foo"), copy.get("foo")); + assertEquals(state.get("baz"), copy.get("baz")); + assertEquals(state.availableKeys().size(), copy.availableKeys().size()); + assertTrue(state.availableKeys().containsAll(copy.availableKeys())); + assertTrue(copy.availableKeys().containsAll(state.availableKeys())); + } + + @Test + public void testParcelable() { + long updatedAt = System.currentTimeMillis(); + long createdAt = updatedAt + 10; + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .objectId("fake") + .createdAt(new Date(createdAt)) + .updatedAt(new Date(updatedAt)) + .isComplete(true) + .put("foo", "bar") + .put("baz", "qux") + .availableKeys(Arrays.asList("safe", "keys")) + .build(); + + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, ParseParcelEncoder.get()); + parcel.setDataPosition(0); + ParseObject.State copy = ParseObject.State.createFromParcel(parcel, ParseParcelDecoder.get()); + + assertEquals(state.className(), copy.className()); + assertEquals(state.objectId(), copy.objectId()); + assertEquals(state.createdAt(), copy.createdAt()); + assertEquals(state.updatedAt(), copy.updatedAt()); + assertEquals(state.isComplete(), copy.isComplete()); + assertEquals(state.keySet().size(), copy.keySet().size()); + assertEquals(state.get("foo"), copy.get("foo")); + assertEquals(state.get("baz"), copy.get("baz")); + assertEquals(state.availableKeys().size(), copy.availableKeys().size()); + assertTrue(state.availableKeys().containsAll(copy.availableKeys())); + assertTrue(copy.availableKeys().containsAll(state.availableKeys())); + } + + @Test + public void testAutomaticUpdatedAt() { + long createdAt = System.currentTimeMillis(); + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .createdAt(new Date(createdAt)) + .build(); + assertEquals(createdAt, state.createdAt()); + assertEquals(createdAt, state.updatedAt()); + } + + @Test + public void testServerData() { + ParseObject.State.Builder builder = new ParseObject.State.Builder("TestObject"); + ParseObject.State state = builder.build(); + assertTrue(state.keySet().isEmpty()); + + builder.put("foo", "bar") + .put("baz", "qux"); + state = builder.build(); + assertEquals(2, state.keySet().size()); + assertEquals("bar", state.get("foo")); + assertEquals("qux", state.get("baz")); + + builder.remove("foo"); + state = builder.build(); + assertEquals(1, state.keySet().size()); + assertNull(state.get("foo")); + assertEquals("qux", state.get("baz")); + + builder.clear(); + state = builder.build(); + assertTrue(state.keySet().isEmpty()); + assertNull(state.get("foo")); + assertNull(state.get("baz")); + } + + @Test + public void testToString() { + String string = new ParseObject.State.Builder("TestObject").build().toString(); + assertTrue(string.contains("com.parse.ParseObject$State")); + assertTrue(string.contains("className")); + assertTrue(string.contains("objectId")); + assertTrue(string.contains("createdAt")); + assertTrue(string.contains("updatedAt")); + assertTrue(string.contains("isComplete")); + assertTrue(string.contains("serverData")); + assertTrue(string.contains("availableKeys")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectTest.java new file mode 100644 index 0000000..bb11aef --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -0,0 +1,793 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import bolts.Capture; +import bolts.Task; +import bolts.TaskCompletionSource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyList; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseObjectTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() { + ParseFieldOperations.registerDefaultDecoders(); // to test JSON / Parcel decoding + } + + @After + public void tearDown() { + ParseCorePlugins.getInstance().reset(); + } + + @Test + public void testFromJSONPayload() throws JSONException { + JSONObject json = new JSONObject( + "{" + + "\"className\":\"GameScore\"," + + "\"createdAt\":\"2015-06-22T21:23:41.733Z\"," + + "\"objectId\":\"TT1ZskATqS\"," + + "\"updatedAt\":\"2015-06-22T22:06:18.104Z\"," + + "\"score\":{" + + "\"__op\":\"Increment\"," + + "\"amount\":1" + + "}," + + "\"age\":33" + + "}"); + + ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); + assertEquals("GameScore", parseObject.getClassName()); + assertEquals("TT1ZskATqS", parseObject.getObjectId()); + ParseDateFormat format = ParseDateFormat.getInstance(); + assertTrue(parseObject.getCreatedAt().equals(format.parse("2015-06-22T21:23:41.733Z"))); + assertTrue(parseObject.getUpdatedAt().equals(format.parse("2015-06-22T22:06:18.104Z"))); + + Set keys = parseObject.getState().keySet(); + assertEquals(0, keys.size()); + + ParseOperationSet currentOperations = parseObject.operationSetQueue.getLast(); + assertEquals(2, currentOperations.size()); + } + + @Test + public void testFromJSONPayloadWithoutClassname() throws JSONException { + JSONObject json = new JSONObject("{\"objectId\":\"TT1ZskATqS\"}"); + ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); + assertNull(parseObject); + } + + //region testRevert + + @Test + public void testRevert() throws ParseException { + List> tasks = new ArrayList<>(); + + // Mocked to let save work + mockCurrentUserController(); + + // Mocked to simulate in-flight save + TaskCompletionSource tcs = mockObjectControllerForSave(); + + // New clean object + ParseObject object = new ParseObject("TestObject"); + object.revert("foo"); + + // Reverts changes on new object + object.put("foo", "bar"); + object.put("name", "grantland"); + object.revert(); + assertNull(object.get("foo")); + assertNull(object.get("name")); + + // Object from server + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.objectId()).thenReturn("test_id"); + when(state.keySet()).thenReturn(Collections.singleton("foo")); + when(state.get("foo")).thenReturn("bar"); + object = ParseObject.from(state); + object.revert(); + assertFalse(object.isDirty()); + assertEquals("bar", object.get("foo")); + + // Reverts changes on existing object + object.put("foo", "baz"); + object.put("name", "grantland"); + object.revert(); + assertFalse(object.isDirty()); + assertEquals("bar", object.get("foo")); + assertFalse(object.isDataAvailable("name")); + + // Shouldn't revert changes done before last call to `save` + object.put("foo", "baz"); + object.put("name", "nlutsenko"); + tasks.add(object.saveInBackground()); + object.revert(); + assertFalse(object.isDirty()); + assertEquals("baz", object.get("foo")); + assertEquals("nlutsenko", object.get("name")); + + // Should revert changes done after last call to `save` + object.put("foo", "qux"); + object.put("name", "grantland"); + object.revert(); + assertFalse(object.isDirty()); + assertEquals("baz", object.get("foo")); + assertEquals("nlutsenko", object.get("name")); + + // Allow save to complete + tcs.setResult(state); + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + + @Test + public void testRevertKey() throws ParseException { + List> tasks = new ArrayList<>(); + + // Mocked to let save work + mockCurrentUserController(); + + // Mocked to simulate in-flight save + TaskCompletionSource tcs = mockObjectControllerForSave(); + + // New clean object + ParseObject object = new ParseObject("TestObject"); + object.revert("foo"); + + // Reverts changes on new object + object.put("foo", "bar"); + object.put("name", "grantland"); + object.revert("foo"); + assertNull(object.get("foo")); + assertEquals("grantland", object.get("name")); + + // Object from server + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.objectId()).thenReturn("test_id"); + when(state.keySet()).thenReturn(Collections.singleton("foo")); + when(state.get("foo")).thenReturn("bar"); + object = ParseObject.from(state); + object.revert("foo"); + assertFalse(object.isDirty()); + assertEquals("bar", object.get("foo")); + + // Reverts changes on existing object + object.put("foo", "baz"); + object.put("name", "grantland"); + object.revert("foo"); + assertEquals("bar", object.get("foo")); + assertEquals("grantland", object.get("name")); + + // Shouldn't revert changes done before last call to `save` + object.put("foo", "baz"); + object.put("name", "nlutsenko"); + tasks.add(object.saveInBackground()); + object.revert("foo"); + assertEquals("baz", object.get("foo")); + assertEquals("nlutsenko", object.get("name")); + + // Should revert changes done after last call to `save` + object.put("foo", "qux"); + object.put("name", "grantland"); + object.revert("foo"); + assertEquals("baz", object.get("foo")); + assertEquals("grantland", object.get("name")); + + // Allow save to complete + tcs.setResult(state); + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + + //endregion + + //region testGetter + + @Test( expected = IllegalStateException.class ) + public void testGetUnavailable() { + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.isComplete()).thenReturn(false); + ParseObject object = ParseObject.from(state); + object.get("foo"); + } + + @Test + public void testGetAvailableIfKeyAvailable() { + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.isComplete()).thenReturn(false); + when(state.availableKeys()).thenReturn(new HashSet<>(Arrays.asList("foo"))); + ParseObject object = ParseObject.from(state); + object.get("foo"); + } + + @Test + public void testGetList() throws Exception { + ParseObject object = new ParseObject("Test"); + JSONArray array = new JSONArray(); + array.put("value"); + array.put("valueAgain"); + object.put("key", array); + + List list = object.getList("key"); + + assertEquals(2, list.size()); + assertTrue(list.contains("value")); + assertTrue(list.contains("valueAgain")); + } + + @Test + public void testGetListWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getList("key")); + } + + @Test + public void testGetJSONArray() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", Arrays.asList("value", "valueAgain")); + + JSONArray array = object.getJSONArray("key"); + + assertEquals(2, array.length()); + assertEquals("value", array.getString(0)); + assertEquals("valueAgain", array.getString(1)); + } + + @Test + public void testGetJsonArrayWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getJSONArray("key")); + } + + @Test + public void testGetJSONObject() throws Exception { + ParseObject object = new ParseObject("Test"); + Map map = new HashMap<>(); + map.put("key", "value"); + map.put("keyAgain", "valueAgain"); + object.put("key", map); + + JSONObject json = object.getJSONObject("key"); + + assertEquals(2, json.length()); + assertEquals("value", json.getString("key")); + assertEquals("valueAgain", json.getString("keyAgain")); + } + + @Test + public void testGetJsonObjectWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getJSONObject("key")); + } + + @Test + public void testGetBoolean() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", true); + + assertTrue(object.getBoolean("key")); + } + + @Test + public void testGetBooleanWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertFalse(object.getBoolean("key")); + } + + @Test + public void testGetDate() throws Exception { + ParseObject object = new ParseObject("Test"); + Date date = new Date(); + object.put("key", date); + + assertEquals(date, object.getDate("key")); + } + + @Test + public void testGetDateWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getDate("key")); + } + + @Test + public void testGetParseGeoPoint() throws Exception { + ParseObject object = new ParseObject("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + object.put("key", point); + + assertEquals(point, object.getParseGeoPoint("key")); + } + + @Test + public void testGetParseGeoPointWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getParseGeoPoint("key")); + } + + @Test + public void testGetParsePolygon() throws Exception { + ParseObject object = new ParseObject("Test"); + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParsePolygon polygon = new ParsePolygon(points); + object.put("key", polygon); + + assertEquals(polygon, object.getParsePolygon("key")); + } + + @Test + public void testGetParsePolygonWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getParsePolygon("key")); + } + + @Test + public void testGetACL() throws Exception { + ParseObject object = new ParseObject("Test"); + ParseACL acl = new ParseACL(); + object.put("ACL", acl); + + assertEquals(acl, object.getACL()); + } + + @Test + public void testGetACLWithSharedACL() throws Exception { + ParseObject object = new ParseObject("Test"); + ParseACL acl = new ParseACL(); + acl.setShared(true); + acl.setPublicReadAccess(true); + object.put("ACL", acl); + + ParseACL aclAgain = object.getACL(); + assertTrue(aclAgain.getPublicReadAccess()); + } + + @Test + public void testGetACLWithNullValue() throws Exception { + ParseObject object = new ParseObject("Test"); + + assertNull(object.getACL()); + } + + @Test + public void testGetACLWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("ACL", 1); + + thrown.expect(RuntimeException.class); + thrown.expectMessage("only ACLs can be stored in the ACL key"); + + object.getACL(); + } + + @Test + public void testGetMap() throws Exception { + ParseObject object = new ParseObject("Test"); + JSONObject json = new JSONObject(); + json.put("key", "value"); + json.put("keyAgain", "valueAgain"); + object.put("key", json); + + Map map = object.getMap("key"); + + assertEquals(2, map.size()); + assertEquals("value", map.get("key")); + assertEquals("valueAgain", map.get("keyAgain")); + } + + @Test + public void testGetMapWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getMap("key")); + } + + @Test + public void testGetParseUser() throws Exception { + ParseObject object = new ParseObject("Test"); + ParseUser user = mock(ParseUser.class); + object.put("key", user); + + assertEquals(user, object.getParseUser("key")); + } + + @Test + public void testGetParseUserWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getParseUser("key")); + } + + @Test + public void testGetParseFile() throws Exception { + ParseObject object = new ParseObject("Test"); + ParseFile file = mock(ParseFile.class); + object.put("key", file); + + assertEquals(file, object.getParseFile("key")); + } + + @Test + public void testGetParseFileWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1); + + assertNull(object.getParseFile("key")); + } + + @Test + public void testGetDouble() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 1.1); + + assertEquals(1.1, object.getDouble("key"), 0.00001); + } + + @Test + public void testGetDoubleWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", "str"); + + assertEquals(0.0, object.getDouble("key"), 0.00001); + } + + @Test + public void testGetLong() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", 10L); + + assertEquals(10L, object.getLong("key")); + } + + @Test + public void testGetLongWithWrongValue() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("key", "str"); + + assertEquals(0, object.getLong("key")); + } + + //endregion + + //region testParcelable + + @Test + public void testParcelable() throws Exception { + ParseObject object = ParseObject.createWithoutData("Test", "objectId"); + object.isDeleted = true; + object.put("long", 200L); + object.put("double", 30D); + object.put("int", 50); + object.put("string", "test"); + object.put("date", new Date(200)); + object.put("null", JSONObject.NULL); + // Collection + object.put("collection", Arrays.asList("test1", "test2")); + // Pointer + ParseObject other = ParseObject.createWithoutData("Test", "otherId"); + object.put("pointer", other); + // Map + Map map = new HashMap<>(); + map.put("key1", "value"); + map.put("key2", 50); + object.put("map", map); + // Bytes + byte[] bytes = new byte[2]; + object.put("bytes", bytes); + // ACL + ParseACL acl = new ParseACL(); + acl.setReadAccess("reader", true); + object.setACL(acl); + // Relation + ParseObject related = ParseObject.createWithoutData("RelatedClass", "relatedId"); + ParseRelation rel = new ParseRelation<>(object, "relation"); + rel.add(related); + object.put("relation", rel); + // File + ParseFile file = new ParseFile(new ParseFile.State.Builder().url("fileUrl").build()); + object.put("file", file); + // GeoPoint + ParseGeoPoint point = new ParseGeoPoint(30d, 50d); + object.put("point", point); + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); + assertEquals(newObject.getClassName(), object.getClassName()); + assertEquals(newObject.isDeleted, object.isDeleted); + assertEquals(newObject.hasChanges(), object.hasChanges()); + assertEquals(newObject.getLong("long"), object.getLong("long")); + assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); + assertEquals(newObject.getInt("int"), object.getInt("int")); + assertEquals(newObject.getString("string"), object.getString("string")); + assertEquals(newObject.getDate("date"), object.getDate("date")); + assertEquals(newObject.get("null"), object.get("null")); + assertEquals(newObject.getList("collection"), object.getList("collection")); + assertEquals(newObject.getParseObject("pointer").getClassName(), other.getClassName()); + assertEquals(newObject.getParseObject("pointer").getObjectId(), other.getObjectId()); + assertEquals(newObject.getMap("map"), object.getMap("map")); + assertEquals(newObject.getBytes("bytes").length, bytes.length); + assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); + ParseRelation newRel = newObject.getRelation("relation"); + assertEquals(newRel.getKey(), rel.getKey()); + assertEquals(newRel.getKnownObjects().size(), rel.getKnownObjects().size()); + newRel.hasKnownObject(related); + assertEquals(newObject.getParseFile("file").getUrl(), object.getParseFile("file").getUrl()); + assertEquals(newObject.getParseGeoPoint("point").getLatitude(), + object.getParseGeoPoint("point").getLatitude(), 0); + } + + @Test + public void testParcelWithCircularReference() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseObject child = new ParseObject("Child"); + parent.setObjectId("parentId"); + parent.put("self", parent); + child.setObjectId("childId"); + child.put("self", child); + child.put("parent", parent); + parent.put("child", child); + + Parcel parcel = Parcel.obtain(); + parent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + parent = ParseObject.CREATOR.createFromParcel(parcel); + assertEquals(parent.getObjectId(), "parentId"); + assertEquals(parent.getParseObject("self").getObjectId(), "parentId"); + child = parent.getParseObject("child"); + assertEquals(child.getObjectId(), "childId"); + assertEquals(child.getParseObject("self").getObjectId(), "childId"); + assertEquals(child.getParseObject("parent").getObjectId(), "parentId"); + } + + @Test + public void testParcelWithCircularReferenceFromServer() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseObject child = new ParseObject("Child"); + parent.setState(new ParseObject.State.Builder("Parent") + .objectId("parentId") + .put("self", parent) + .put("child", child).build()); + parent.setObjectId("parentId"); + child.setState(new ParseObject.State.Builder("Child") + .objectId("childId") + .put("self", child) + .put("parent", parent).build()); + + Parcel parcel = Parcel.obtain(); + parent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + parent = ParseObject.CREATOR.createFromParcel(parcel); + assertEquals(parent.getObjectId(), "parentId"); + assertEquals(parent.getParseObject("self").getObjectId(), "parentId"); + child = parent.getParseObject("child"); + assertEquals(child.getObjectId(), "childId"); + assertEquals(child.getParseObject("self").getObjectId(), "childId"); + assertEquals(child.getParseObject("parent").getObjectId(), "parentId"); + } + + @Test + public void testParcelWhileSaving() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); + + // Create multiple ParseOperationSets + List> tasks = new ArrayList<>(); + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + object.put("key", "value"); + object.put("number", 5); + tasks.add(object.saveInBackground()); + + object.put("key", "newValue"); + object.increment("number", 6); + tasks.add(object.saveInBackground()); + + object.increment("number", -1); + tasks.add(object.saveInBackground()); + + // Ensure Log.w is called... + assertTrue(object.hasOutstandingOperations()); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(other.isDirty("key")); + assertTrue(other.isDirty("number")); + assertEquals(other.getString("key"), "newValue"); + assertEquals(other.getNumber("number"), 10); + // By design, when LDS is off, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.hasOutstandingOperations()); + + // Force finish save operations on the old instance. + tcs.setResult(null); + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + + @Test + public void testParcelWhileSavingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + + object.put("key", "value"); + object.increment("number", 3); + Task saveTask = object.saveInBackground(); + assertTrue(object.hasOutstandingOperations()); // Saving + assertFalse(object.isDirty()); // Not dirty because it's saving + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.hasOutstandingOperations()); // Still saving + assertFalse(other.isDirty()); // Still not dirty + assertEquals(other.getNumber("number"), 3); + + tcs.setResult(null); + saveTask.waitForCompletion(); + Parse.setLocalDatastore(null); + } + + @Test + public void testParcelWhileDeleting() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + Task deleteTask = object.deleteInBackground(); + + // ensure Log.w is called.. + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + // By design, when LDS is off, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.isDeleting); + assertTrue(object.isDeleting); + + tcs.setResult(null); + deleteTask.waitForCompletion(); + assertFalse(object.isDeleting); + assertTrue(object.isDeleted); + } + + @Test + public void testParcelWhileDeletingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + Task deleteTask = object.deleteInBackground(); + + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.isDeleting); // Still deleting + + tcs.setResult(null); + deleteTask.waitForCompletion(); // complete deletion on original object. + assertFalse(other.isDeleting); + assertTrue(other.isDeleted); + Parse.setLocalDatastore(null); + } + + //endregion + + private static void mockCurrentUserController() { + ParseCurrentUserController userController = mock(ParseCurrentUserController.class); + when(userController.getCurrentSessionTokenAsync()).thenReturn(Task.forResult("token")); + when(userController.getAsync()).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(userController); + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForSave() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.saveAsync( + any(ParseObject.State.class), any(ParseOperationSet.class), + anyString(), any(ParseDecoder.class)) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForDelete() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.deleteAsync( + any(ParseObject.State.class), anyString()) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseOkHttpClientTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseOkHttpClientTest.java new file mode 100644 index 0000000..286862d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseOkHttpClientTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okio.Buffer; +import okio.BufferedSource; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseOkHttpClientTest { + + private MockWebServer server = new MockWebServer(); + + //region testTransferRequest/Response + + @Test + public void testGetOkHttpRequestType() throws IOException { + ParseHttpClient parseClient = ParseHttpClient.createClient(new OkHttpClient.Builder()); + ParseHttpRequest.Builder builder = new ParseHttpRequest.Builder(); + builder.setUrl("http://www.parse.com"); + + // Get + ParseHttpRequest parseRequest = builder + .setMethod(ParseHttpRequest.Method.GET) + .setBody(null) + .build(); + Request okHttpRequest = parseClient.getRequest(parseRequest); + assertEquals(ParseHttpRequest.Method.GET.toString(), okHttpRequest.method()); + + // Post + parseRequest = builder + .setMethod(ParseHttpRequest.Method.POST) + .setBody(new ParseByteArrayHttpBody("test", "application/json")) + .build(); + okHttpRequest = parseClient.getRequest(parseRequest); + assertEquals(ParseHttpRequest.Method.POST.toString(), okHttpRequest.method()); + + // Delete + parseRequest = builder + .setMethod(ParseHttpRequest.Method.DELETE) + .setBody(null) + .build(); + okHttpRequest = parseClient.getRequest(parseRequest); + assertEquals(ParseHttpRequest.Method.DELETE.toString(), okHttpRequest.method()); + assertEquals(null, okHttpRequest.body()); + + // Put + parseRequest = builder + .setMethod(ParseHttpRequest.Method.PUT) + .setBody(new ParseByteArrayHttpBody("test", "application/json")) + .build(); + okHttpRequest = parseClient.getRequest(parseRequest); + assertEquals(ParseHttpRequest.Method.PUT.toString(), okHttpRequest.method()); + } + + @Test + public void testGetOkHttpRequest() throws IOException { + Map headers = new HashMap<>(); + String headerName = "name"; + String headerValue = "value"; + headers.put(headerName, headerValue); + + String url = "http://www.parse.com/"; + String content = "test"; + int contentLength = content.length(); + String contentType = "application/json"; + ParseHttpRequest parseRequest = new ParseHttpRequest.Builder() + .setUrl(url) + .setMethod(ParseHttpRequest.Method.POST) + .setBody(new ParseByteArrayHttpBody(content, contentType)) + .setHeaders(headers) + .build(); + + ParseHttpClient parseClient = ParseHttpClient.createClient(new OkHttpClient.Builder()); + Request okHttpRequest = parseClient.getRequest(parseRequest); + + // Verify method + assertEquals(ParseHttpRequest.Method.POST.toString(), okHttpRequest.method()); + // Verify URL + assertEquals(url, okHttpRequest.url().toString()); + // Verify Headers + assertEquals(1, okHttpRequest.headers(headerName).size()); + assertEquals(headerValue, okHttpRequest.headers(headerName).get(0)); + // Verify Body + RequestBody okHttpBody = okHttpRequest.body(); + assertEquals(contentLength, okHttpBody.contentLength()); + assertEquals(contentType, okHttpBody.contentType().toString()); + // Can not read parseRequest body to compare since it has been read during + // creating okHttpRequest + Buffer buffer = new Buffer(); + okHttpBody.writeTo(buffer); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + buffer.copyTo(output); + assertArrayEquals(content.getBytes(), output.toByteArray()); + } + + @Test + public void testGetOkHttpRequestWithEmptyContentType() throws Exception { + String url = "http://www.parse.com/"; + String content = "test"; + ParseHttpRequest parseRequest = new ParseHttpRequest.Builder() + .setUrl(url) + .setMethod(ParseHttpRequest.Method.POST) + .setBody(new ParseByteArrayHttpBody(content, null)) + .build(); + + ParseHttpClient parseClient = ParseHttpClient.createClient(new OkHttpClient.Builder()); + Request okHttpRequest = parseClient.getRequest(parseRequest); + + // Verify Content-Type + assertNull(okHttpRequest.body().contentType()); + } + + @Test + public void testGetParseResponse() throws IOException { + int statusCode = 200; + String reasonPhrase = "test reason"; + final String content = "test"; + final int contentLength = content.length(); + final String contentType = "application/json"; + String url = "http://www.parse.com/"; + Request request = new Request.Builder() + .url(url) + .build(); + Response okHttpResponse = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(statusCode) + .message(reasonPhrase) + .body(new ResponseBody() { + @Override + public MediaType contentType() { + return MediaType.parse(contentType); + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public BufferedSource source() { + Buffer buffer = new Buffer(); + buffer.write(content.getBytes()); + return buffer; + } + }) + .build(); + + ParseHttpClient parseClient = ParseHttpClient.createClient(new OkHttpClient.Builder()); + ParseHttpResponse parseResponse = parseClient.getResponse(okHttpResponse); + + // Verify status code + assertEquals(statusCode, parseResponse.getStatusCode()); + // Verify reason phrase + assertEquals(reasonPhrase, parseResponse.getReasonPhrase()); + // Verify content length + assertEquals(contentLength, parseResponse.getTotalSize()); + // Verify content + assertArrayEquals(content.getBytes(), ParseIOUtils.toByteArray(parseResponse.getContent())); + } + + //endregion + + //region testOkHttpClientWithInterceptor + + @Test + public void testParseOkHttpClientExecuteWithGZIPResponse() throws Exception { + // Make mock response + Buffer buffer = new Buffer(); + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + GZIPOutputStream gzipOut = new GZIPOutputStream(byteOut); + gzipOut.write("content".getBytes()); + gzipOut.close(); + buffer.write(byteOut.toByteArray()); + MockResponse mockResponse = new MockResponse() + .setStatus("HTTP/1.1 " + 201 + " " + "OK") + .setBody(buffer) + .setHeader("Content-Encoding", "gzip"); + + // Start mock server + server.enqueue(mockResponse); + server.start(); + + ParseHttpClient client = ParseHttpClient.createClient(new OkHttpClient.Builder()); + + // We do not need to add Accept-Encoding header manually, httpClient library should do that. + String requestUrl = server.url("/").toString(); + ParseHttpRequest parseRequest = new ParseHttpRequest.Builder() + .setUrl(requestUrl) + .setMethod(ParseHttpRequest.Method.GET) + .build(); + + // Execute request + ParseHttpResponse parseResponse = client.execute(parseRequest); + + // Make sure the response we get is ungziped by OkHttp library + byte[] content = ParseIOUtils.toByteArray(parseResponse.getContent()); + assertArrayEquals("content".getBytes(), content); + + server.shutdown(); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePolygonTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePolygonTest.java new file mode 100644 index 0000000..181e2ff --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePolygonTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParsePolygonTest { + + @Test + public void testConstructors() { + List arrayPoints = Arrays.asList( + new ParseGeoPoint(0,0), + new ParseGeoPoint(0,1), + new ParseGeoPoint(1,1), + new ParseGeoPoint(1,0) + ); + + List listPoints = new ArrayList(); + listPoints.add(new ParseGeoPoint(0,0)); + listPoints.add(new ParseGeoPoint(0,1)); + listPoints.add(new ParseGeoPoint(1,1)); + listPoints.add(new ParseGeoPoint(1,0)); + + ParsePolygon polygonList = new ParsePolygon(listPoints); + assertEquals(listPoints, polygonList.getCoordinates()); + + ParsePolygon polygonArray = new ParsePolygon(arrayPoints); + assertEquals(arrayPoints, polygonArray.getCoordinates()); + + ParsePolygon copyList = new ParsePolygon(polygonList); + assertEquals(polygonList.getCoordinates(), copyList.getCoordinates()); + + ParsePolygon copyArray = new ParsePolygon(polygonArray); + assertEquals(polygonArray.getCoordinates(), copyArray.getCoordinates()); + } + + @Test(expected = IllegalArgumentException.class) + public void testThreePointMinimum() { + ParseGeoPoint p1 = new ParseGeoPoint(0,0); + ParseGeoPoint p2 = new ParseGeoPoint(0,1); + List points = Arrays.asList(p1,p2); + ParsePolygon polygon = new ParsePolygon(points); + } + + @Test + public void testEquality() { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + List diff = new ArrayList(); + diff.add(new ParseGeoPoint(0,0)); + diff.add(new ParseGeoPoint(0,10)); + diff.add(new ParseGeoPoint(10,10)); + diff.add(new ParseGeoPoint(10,0)); + diff.add(new ParseGeoPoint(0,0)); + + ParsePolygon polygonA = new ParsePolygon(points); + ParsePolygon polygonB = new ParsePolygon(points); + ParsePolygon polygonC = new ParsePolygon(diff); + + assertTrue(polygonA.equals(polygonB)); + assertTrue(polygonA.equals(polygonA)); + assertTrue(polygonB.equals(polygonA)); + + assertFalse(polygonA.equals(null)); + assertFalse(polygonA.equals(true)); + assertFalse(polygonA.equals(polygonC)); + } + + @Test + public void testContainsPoint() { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + + ParseGeoPoint inside = new ParseGeoPoint(0.5,0.5); + ParseGeoPoint outside = new ParseGeoPoint(10,10); + + ParsePolygon polygon = new ParsePolygon(points); + + assertTrue(polygon.containsPoint(inside)); + assertFalse(polygon.containsPoint(outside)); + } + + @Test + public void testParcelable() { + List points = new ArrayList(); + points.add(new ParseGeoPoint(0,0)); + points.add(new ParseGeoPoint(0,1)); + points.add(new ParseGeoPoint(1,1)); + points.add(new ParseGeoPoint(1,0)); + ParsePolygon polygon = new ParsePolygon(points); + Parcel parcel = Parcel.obtain(); + polygon.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + polygon = ParsePolygon.CREATOR.createFromParcel(parcel); + assertEquals(polygon.getCoordinates(), points); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushControllerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushControllerTest.java new file mode 100644 index 0000000..65623e3 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushControllerTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import bolts.Task; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +// For SSLSessionCache +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParsePushControllerTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testBuildRESTSendPushCommand + + @Test + public void testBuildRESTSendPushCommandWithChannelSet() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + List expectedChannelSet = new ArrayList(){{ + add("foo"); + add("bar"); + add("yarr"); + }}; + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .channelSet(expectedChannelSet) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + // Verify device type and query + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_WHERE)); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + JSONArray pushChannels = jsonParameters.getJSONArray(ParseRESTPushCommand.KEY_CHANNELS); + assertEquals(new JSONArray(expectedChannelSet), pushChannels, + JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testBuildRESTSendPushCommandWithExpirationTime() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .expirationTime((long) 1400000000) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + // Verify device type and query + assertEquals("{}", jsonParameters.get(ParseRESTPushCommand.KEY_WHERE).toString()); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertEquals(1400000000, jsonParameters.getLong(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + } + + @Test + public void testBuildRESTSendPushCommandWithPushTime() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + long pushTime = System.currentTimeMillis() / 1000 + 1000; + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .pushTime(pushTime) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + // Verify device type and query + assertEquals("{}", jsonParameters.get(ParseRESTPushCommand.KEY_WHERE).toString()); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertEquals(pushTime, jsonParameters.getLong(ParseRESTPushCommand.KEY_PUSH_TIME)); + } + + @Test + public void testBuildRESTSendPushCommandWithExpirationTimeInterval() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .expirationTimeInterval((long) 86400) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + // Verify device type and query + assertEquals("{}", jsonParameters.get(ParseRESTPushCommand.KEY_WHERE).toString()); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertEquals(86400, jsonParameters.getLong(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + } + + @Test + public void testBuildRESTSendPushCommandWithQuery() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParseQuery query = ParseInstallation.getQuery(); + query.whereEqualTo("language", "en/US"); + query.whereLessThan("version", "1.2"); + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .query(query) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_WHERE) + .has(ParseRESTPushCommand.KEY_DEVICE_TYPE)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + assertEquals("hello world", + jsonParameters + .getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + + JSONObject pushQuery = jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_WHERE); + assertEquals("en/US", pushQuery.getString("language")); + JSONObject inequality = pushQuery.getJSONObject("version"); + assertEquals("1.2", inequality.getString("$lt")); + } + + @Test + public void testBuildRESTSendPushCommandWithPushToAndroid() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .pushToAndroid(true) + .data(data) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertEquals(ParsePushController.DEVICE_TYPE_ANDROID, + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_WHERE) + .optString(ParseRESTPushCommand.KEY_DEVICE_TYPE, null)); + } + + @Test + public void testBuildRESTSendPushCommandWithPushToIOS() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .pushToIOS(true) + .data(data) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertEquals(ParsePushController.DEVICE_TYPE_IOS, + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_WHERE) + .optString(ParseRESTPushCommand.KEY_DEVICE_TYPE, null)); + } + + @Test + public void testBuildRESTSendPushCommandWithPushToIOSAndAndroid() throws Exception { + ParseHttpClient restClient = mock(ParseHttpClient.class); + ParsePushController controller = new ParsePushController(restClient); + + // Build PushState + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .pushToAndroid(true) + .pushToIOS(true) + .data(data) + .build(); + + // Build command + ParseRESTCommand pushCommand = controller.buildRESTSendPushCommand(state, "sessionToken"); + + // Verify command + JSONObject jsonParameters = pushCommand.jsonParameters; + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_PUSH_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_TIME)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_EXPIRATION_INTERVAL)); + assertFalse(jsonParameters.has(ParseRESTPushCommand.KEY_CHANNELS)); + assertEquals("hello world", + jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_DATA) + .getString(ParsePush.KEY_DATA_MESSAGE)); + assertFalse(jsonParameters.getJSONObject(ParseRESTPushCommand.KEY_WHERE) + .has(ParseRESTPushCommand.KEY_DEVICE_TYPE)); + } + + //endregion + + //region testSendInBackground + + @Test + public void testSendInBackgroundSuccess() throws Exception { + JSONObject mockResponse = new JSONObject(); + mockResponse.put("result", "OK"); + ParseHttpClient restClient = mockParseHttpClientWithResponse(mockResponse, 200, "OK"); + ParsePushController controller = new ParsePushController(restClient); + + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .build(); + + Task pushTask= controller.sendInBackground(state, "sessionToken"); + + // Verify task complete + ParseTaskUtils.wait(pushTask); + // Verify httpclient execute encough times + verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); + } + + @Test + public void testSendInBackgroundFailWithIOException() throws Exception { + // TODO(mengyan): Remove once we no longer rely on retry logic. + ParseRequest.setDefaultInitialRetryDelay(1L); + + ParseHttpClient restClient = mock(ParseHttpClient.class); + when(restClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + ParsePushController controller = new ParsePushController(restClient); + + JSONObject data = new JSONObject(); + data.put(ParsePush.KEY_DATA_MESSAGE, "hello world"); + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .build(); + + Task pushTask= controller.sendInBackground(state, "sessionToken"); + + // Do not use ParseTaskUtils.wait() since we do not want to throw the exception + pushTask.waitForCompletion(); + // Verify httpClient is tried enough times + // TODO(mengyan): Abstract out command runner so we don't have to account for retries. + verify(restClient, times(5)).execute(any(ParseHttpRequest.class)); + assertTrue(pushTask.isFaulted()); + Exception error = pushTask.getError(); + assertThat(error, instanceOf(ParseException.class)); + assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) error).getCode()); + } + + //endregion + + private ParseHttpClient mockParseHttpClientWithResponse(JSONObject content, int statusCode, + String reasonPhrase) throws IOException { + byte[] contentBytes = content.toString().getBytes(); + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(contentBytes)) + .setStatusCode(statusCode) + .setTotalSize(contentBytes.length) + .setContentType("application/json") + .build(); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + return client; + } + + private static boolean containsString(JSONArray array, String value) throws JSONException { + for (int i = 0; i < array.length(); i++) { + Object element = array.get(i); + if (element instanceof String && element.equals(value)) { + return true; + } + } + return false; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushStateTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushStateTest.java new file mode 100644 index 0000000..bae68b2 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushStateTest.java @@ -0,0 +1,502 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.mockito.internal.util.collections.Sets; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ParsePushStateTest { + + //region testDefaults + + @Test(expected = IllegalArgumentException.class) + public void testDefaultsWithoutData() throws Exception { + // We have to set data to a state otherwise it will throw an exception + JSONObject data = new JSONObject(); + + ParsePush.State state = new ParsePush.State.Builder() + .build(); + } + + @Test + public void testDefaultsWithData() throws Exception { + // We have to set data to a state otherwise it will throw an exception + JSONObject data = new JSONObject(); + + ParsePush.State state = new ParsePush.State.Builder() + .data(data) + .build(); + + assertEquals(null, state.expirationTime()); + assertEquals(null, state.expirationTimeInterval()); + assertEquals(null, state.pushTime()); + assertEquals(null, state.channelSet()); + JSONAssert.assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(null, state.pushToAndroid()); + assertEquals(null, state.pushToIOS()); + assertEquals(null, state.queryState()); + } + + //endregion + + @Test + public void testCopy() throws JSONException { + ParsePush.State state = mock(ParsePush.State.class); + when(state.expirationTime()).thenReturn(1L); + when(state.expirationTimeInterval()).thenReturn(2L); + when(state.pushTime()).thenReturn(3L); + Set channelSet = Sets.newSet("one", "two"); + when(state.channelSet()).thenReturn(channelSet); + JSONObject data = new JSONObject(); + data.put("foo", "bar"); + when(state.data()).thenReturn(data); + when(state.pushToAndroid()).thenReturn(true); + when(state.pushToIOS()).thenReturn(false); + ParseQuery.State queryState = + new ParseQuery.State.Builder<>(ParseInstallation.class).build(); + when(state.queryState()).thenReturn(queryState); + + ParsePush.State copy = new ParsePush.State.Builder(state).build(); + assertSame(1L, copy.expirationTime()); + assertSame(2L, copy.expirationTimeInterval()); + assertSame(3L, copy.pushTime()); + Set channelSetCopy = copy.channelSet(); + assertNotSame(channelSet, channelSetCopy); + assertTrue(channelSetCopy.size() == 2 && channelSetCopy.contains("one")); + JSONObject dataCopy = copy.data(); + assertNotSame(data, dataCopy); + assertEquals("bar", dataCopy.get("foo")); + assertTrue(copy.pushToAndroid()); + assertFalse(copy.pushToIOS()); + ParseQuery.State queryStateCopy = copy.queryState(); + assertNotSame(queryState, queryStateCopy); + assertEquals("_Installation", queryStateCopy.className()); + } + + //region testExpirationTime + + @Test + public void testExpirationTimeNullTime() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .expirationTime(null) + .data(new JSONObject()) + .build(); + + assertEquals(null, state.expirationTime()); + } + + @Test + public void testExpirationTimeNormalTime() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .expirationTime(100L) + .data(new JSONObject()) + .build(); + + assertEquals(100L, state.expirationTime().longValue()); + } + + //endregion + + //region testExpirationTimeInterval + + @Test + public void testExpirationTimeIntervalNullInterval() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .expirationTimeInterval(null) + .data(new JSONObject()) + .build(); + + assertEquals(null, state.expirationTimeInterval()); + } + + @Test + public void testExpirationTimeIntervalNormalInterval() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .expirationTimeInterval(100L) + .data(new JSONObject()) + .build(); + + assertEquals(100L, state.expirationTimeInterval().longValue()); + } + + //endregion + + //region testPushTime + + @Test + public void testPushTimeNullTime() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushTime(null) + .data(new JSONObject()) + .build(); + + assertEquals(null, state.pushTime()); + } + + @Test + public void testPushTimeNormalTime() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + long time = System.currentTimeMillis() / 1000 + 1000; + ParsePush.State state = builder + .pushTime(time) + .data(new JSONObject()) + .build(); + + assertEquals(time, state.pushTime().longValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPushTimeInThePast() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushTime(System.currentTimeMillis() / 1000 - 1000) + .data(new JSONObject()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testPushTimeTwoWeeksFromNow() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushTime(System.currentTimeMillis() / 1000 + 60*60*24*7*3) + .data(new JSONObject()) + .build(); + } + + //endregion + + //region testChannelSet + + @Test(expected = IllegalArgumentException.class) + public void testChannelSetNullChannelSet() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .channelSet(null) + .data(new JSONObject()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testChannelSetNormalChannelSetWithNullChannel() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + Set channelSet = new HashSet<>(); + channelSet.add(null); + + ParsePush.State state = builder + .channelSet(channelSet) + .data(new JSONObject()) + .build(); + } + + @Test + public void testChannelSetNormalChannelSet() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + Set channelSet = new HashSet<>(); + channelSet.add("foo"); + channelSet.add("bar"); + + ParsePush.State state = builder + .channelSet(channelSet) + .data(new JSONObject()) + .build(); + + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("foo")); + assertTrue(state.channelSet().contains("bar")); + } + + @Test + public void testChannelSetOverwrite() { + Set channelSet = new HashSet<>(); + channelSet.add("foo"); + Set channelSetAgain = new HashSet<>(); + channelSetAgain.add("bar"); + + ParsePush.State state = new ParsePush.State.Builder() + .channelSet(channelSet) + .channelSet(channelSetAgain) + .data(new JSONObject()) + .build(); + + assertEquals(1, state.channelSet().size()); + assertTrue(state.channelSet().contains("bar")); + } + + @Test + public void testChannelSetDuplicateChannel() { + final List channelSet = new ArrayList(){{ + add("foo"); + add("foo"); + }}; + ParsePush.State state = new ParsePush.State.Builder() + .channelSet(channelSet) + .data(new JSONObject()) + .build(); + + assertEquals(1, state.channelSet().size()); + assertTrue(state.channelSet().contains("foo")); + } + + //endregion + + //region testData + + @Test(expected = IllegalArgumentException.class) + public void testDataNullData() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + ParsePush.State state = builder + .data(null) + .build(); + } + + @Test + public void testDataNormalData() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + JSONObject data = new JSONObject(); + data.put("name", "value"); + + ParsePush.State state = builder + .data(data) + .build(); + + JSONObject dataAgain = state.data(); + assertEquals(1, dataAgain.length()); + assertEquals("value", dataAgain.get("name")); + } + + //endregion + + //region testPushToAndroid + + @Test + public void testPushToAndroidNullValue() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushToAndroid(null) + .data(new JSONObject()) + .build(); + + assertEquals(null, state.pushToAndroid()); + } + + @Test + public void testPushToAndroidNormalValue() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushToAndroid(true) + .data(new JSONObject()) + .build(); + + assertTrue(state.pushToAndroid()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPushToAndroidQueryAlreadySet() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .query(ParseInstallation.getQuery()) + .pushToAndroid(true) + .data(new JSONObject()) + .build(); + } + + //endregion + + //region testPushToIOS + + @Test + public void testPushToIOSNullValue() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushToIOS(null) + .data(new JSONObject()) + .build(); + + assertEquals(null, state.pushToIOS()); + } + + @Test + public void testPushToIOSNormalValue() { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushToIOS(true) + .data(new JSONObject()) + .build(); + + assertTrue(state.pushToIOS()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPushToIOSQueryAlreadySet() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .query(ParseInstallation.getQuery()) + .pushToIOS(true) + .data(new JSONObject()) + .build(); + } + + //endregion + + //region testQuery + + @Test(expected = IllegalArgumentException.class) + public void testQueryNullQuery() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .query(null) + .data(new JSONObject()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testQueryPushToIOSPushToAndroidAlreadySet() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .pushToAndroid(true) + .pushToIOS(false) + .query(ParseInstallation.getQuery()) + .data(new JSONObject()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testQueryNotInstallationQuery() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + + ParsePush.State state = builder + .query(new ParseQuery("test")) + .data(new JSONObject()) + .build(); + } + + @Test + public void testQueryNormalQuery() throws Exception { + ParsePush.State.Builder builder = new ParsePush.State.Builder(); + // Normal query + ParseQuery query = ParseInstallation.getQuery(); + // Make test ParseQuery state + ParseQuery.State.Builder subQueryState = + new ParseQuery.State.Builder<>("TestObject"); + query.getBuilder() + .whereEqualTo("foo", "bar") + .whereMatchesQuery("subquery", subQueryState) + .setLimit(12) + .setSkip(34) + .orderByAscending("foo").addDescendingOrder("bar") + .include("name") + .selectKeys(Arrays.asList("name", "blah")) + .setTracingEnabled(true) + .redirectClassNameForKey("what"); + + ParsePush.State state = builder + .query(query) + .data(new JSONObject()) + .build(); + + ParseQuery.State queryStateAgain = state.queryState(); + JSONObject queryStateAgainJson = queryStateAgain.toJSON(PointerEncoder.get()); + assertEquals("_Installation", queryStateAgainJson.getString("className")); + JSONAssert.assertEquals("{" + + "\"foo\":\"bar\"," + + "\"subquery\":{\"$inQuery\":{\"className\":\"TestObject\",\"where\":{}}}" + + "}", queryStateAgainJson.getJSONObject("where"), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(12, queryStateAgainJson.getInt("limit")); + assertEquals(34, queryStateAgainJson.getInt("skip")); + assertEquals("foo,-bar", queryStateAgainJson.getString("order")); + assertEquals("name", queryStateAgainJson.getString("include")); + assertEquals("name,blah", queryStateAgainJson.getString("fields")); + assertEquals(1, queryStateAgainJson.getInt("trace")); + assertEquals("what", queryStateAgainJson.getString("redirectClassNameForKey")); + } + + //endregion + + //region testStateImmutable + + @Test + public void testStateImmutable() throws Exception { + JSONObject data = new JSONObject(); + data.put("name", "value"); + Set channelSet = new HashSet<>(); + channelSet.add("foo"); + channelSet.add("bar"); + ParsePush.State state = new ParsePush.State.Builder() + .channelSet(channelSet) + .data(data) + .build(); + + // Verify channelSet immutable + Set stateChannelSet = state.channelSet(); + try { + stateChannelSet.add("test"); + fail("Should throw an exception"); + } catch (UnsupportedOperationException e) { + // do nothing + } + + channelSet.add("test"); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("foo")); + assertTrue(state.channelSet().contains("bar")); + + // Verify data immutable + JSONObject stateData = state.data(); + stateData.put("foo", "bar"); + JSONObject stateDataAgain = state.data(); + assertEquals(1, stateDataAgain.length()); + assertEquals("value", stateDataAgain.get("name")); + + // Verify queryState immutable + // TODO(mengyan) add test after t6941155(Convert mutable parameter to immutable) + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushTest.java new file mode 100644 index 0000000..d9f5aed --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParsePushTest.java @@ -0,0 +1,823 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import bolts.Capture; +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParsePushTest { + + @Before + public void setUp() { + ParseTestUtils.setTestParseUser(); + } + + @After + public void tearDown() { + ParseCorePlugins.getInstance().reset(); + } + + //region testSetChannel + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetChannel() { + ParsePush push = new ParsePush(); + push.setChannel("test"); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertEquals(1, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + } + + //endregion + + //region testSetChannels + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetChannels() { + ParsePush push = new ParsePush(); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.setChannels(channels); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + + //endregion + + //region testSetData + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetData() throws Exception { + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + data.put("keyAgain", "valueAgain"); + push.setData(data); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + ParsePush.State state = push.builder.build(); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + } + + //endregion + + //region testSetData + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetMessage() throws Exception { + ParsePush push = new ParsePush(); + push.setMessage("test"); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + ParsePush.State state = push.builder.build(); + JSONObject data = state.data(); + assertEquals("test", data.getString(ParsePush.KEY_DATA_MESSAGE)); + } + + //endregion + + //region testSetExpirationTime + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetExpirationTime() throws Exception { + ParsePush push = new ParsePush(); + push.setExpirationTime(10000); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertEquals(10000, state.expirationTime().longValue()); + } + + //endregion + + //region testSetExpirationTimeInterval + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetExpirationTimeInterval() throws Exception { + ParsePush push = new ParsePush(); + push.setExpirationTimeInterval(10000); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertEquals(10000, state.expirationTimeInterval().longValue()); + } + + //endregion + + //region testClearExpiration + + @Test + public void testClearExpiration() { + ParsePush push = new ParsePush(); + push.setExpirationTimeInterval(10000); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + + // Make sure interval has value before clear + ParsePush.State state = push.builder.build(); + assertEquals(10000, state.expirationTimeInterval().longValue()); + + // Make sure interval is empty after clear + push.clearExpiration(); + state = push.builder.build(); + assertNull(state.expirationTimeInterval()); + + push.setExpirationTime(200); + // Make sure expiration time has value before clear + state = push.builder.build(); + assertEquals(200, state.expirationTime().longValue()); + + // Make sure interval is empty after clear + push.clearExpiration(); + state = push.builder.build(); + assertNull(state.expirationTime()); + } + + //endregion + + //region testSetPushTime + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetPushTime() throws Exception { + ParsePush push = new ParsePush(); + long time = System.currentTimeMillis() / 1000 + 1000; + push.setPushTime(time); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertEquals(time, state.pushTime().longValue()); + } + + //endregion + + //region testSetPushToIOS + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetPushToIOS() throws Exception { + ParsePush push = new ParsePush(); + push.setPushToIOS(true); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertTrue(state.pushToIOS()); + } + + //endregion + + //region testSetPushToAndroid + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetPushToAndroid() throws Exception { + ParsePush push = new ParsePush(); + push.setPushToAndroid(true); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + assertTrue(state.pushToAndroid()); + } + + //endregion + + //region testSetQuery + + // We only test a basic case here to make sure logic in ParsePush is correct, more comprehensive + // builder test cases should be in ParsePushState test + @Test + public void testSetQuery() throws Exception { + ParsePush push = new ParsePush(); + ParseQuery query = ParseInstallation.getQuery(); + query.getBuilder() + .whereEqualTo("foo", "bar"); + push.setQuery(query); + + // Right now it is hard for us to test a builder, so we build a state to test the builder is + // set correctly + // We have to set message otherwise build() will throw an exception + push.setMessage("message"); + ParsePush.State state = push.builder.build(); + ParseQuery.State queryState = state.queryState(); + JSONObject queryStateJson = queryState.toJSON(PointerEncoder.get()); + assertEquals("bar", queryStateJson.getJSONObject("where").getString("foo")); + } + + //endregion + + //region testSubscribeInBackground + + @Test + public void testSubscribeInBackgroundSuccess() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + when(controller.subscribeInBackground(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + ParseTaskUtils.wait(ParsePush.subscribeInBackground("test")); + verify(controller, times(1)).subscribeInBackground("test"); + } + + @Test + public void testSubscribeInBackgroundWithCallbackSuccess() throws Exception { + final ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + when(controller.subscribeInBackground(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + ParsePush push = new ParsePush(); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.subscribeInBackground("test", new SaveCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + assertNull(exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).subscribeInBackground("test"); + } + + @Test + public void testSubscribeInBackgroundFail() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.subscribeInBackground(anyString())).thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + Task pushTask = ParsePush.subscribeInBackground("test"); + pushTask.waitForCompletion(); + verify(controller, times(1)).subscribeInBackground("test"); + assertTrue(pushTask.isFaulted()); + assertSame(exception, pushTask.getError()); + } + + @Test + public void testSubscribeInBackgroundWithCallbackFail() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.subscribeInBackground(anyString())).thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + ParsePush push = new ParsePush(); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.subscribeInBackground("test", new SaveCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + assertSame(exception, exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).subscribeInBackground("test"); + } + + //endregion + + //region testUnsubscribeInBackground + + @Test + public void testUnsubscribeInBackgroundSuccess() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + when(controller.unsubscribeInBackground(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + ParseTaskUtils.wait(ParsePush.unsubscribeInBackground("test")); + verify(controller, times(1)).unsubscribeInBackground("test"); + } + + @Test + public void testUnsubscribeInBackgroundWithCallbackSuccess() throws Exception { + final ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + when(controller.unsubscribeInBackground(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.unsubscribeInBackground("test", new SaveCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + assertNull(exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).unsubscribeInBackground("test"); + } + + @Test + public void testUnsubscribeInBackgroundFail() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.unsubscribeInBackground(anyString())) + .thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + Task pushTask = ParsePush.unsubscribeInBackground("test"); + pushTask.waitForCompletion(); + verify(controller, times(1)).unsubscribeInBackground("test"); + assertTrue(pushTask.isFaulted()); + assertSame(exception, pushTask.getError()); + } + + @Test + public void testUnsubscribeInBackgroundWithCallbackFail() throws Exception { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.unsubscribeInBackground(anyString())) + .thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + ParsePush push = new ParsePush(); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.unsubscribeInBackground("test", new SaveCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + assertSame(exception, exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + verify(controller, times(1)).unsubscribeInBackground("test"); + } + + //endregion + + //region testGetPushChannelsController + + @Test + public void testGetPushChannelsController() { + ParsePushChannelsController controller = mock(ParsePushChannelsController.class); + ParseCorePlugins.getInstance().registerPushChannelsController(controller); + + assertSame(controller, ParsePush.getPushChannelsController()); + } + + //endregion + + //region testGetPushController + + @Test + public void testGetPushController() { + ParsePushController controller = mock(ParsePushController.class); + ParseCorePlugins.getInstance().registerPushController(controller); + + assertSame(controller, ParsePush.getPushController()); + } + + //endregion + + //region testSendInBackground + + @Test + public void testSendInBackgroundSuccess() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + ParseTaskUtils.wait(push.sendInBackground()); + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + + @Test + public void testSendInBackgroundWithCallbackSuccess() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + push.sendInBackground(new SendCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + + // Make sure controller is executed and state parameter is correct + assertNull(exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + + @Test + public void testSendInBackgroundFail() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + Task pushTask = push.sendInBackground(); + pushTask.waitForCompletion(); + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + // Make sure task is failed + assertTrue(pushTask.isFaulted()); + assertSame(exception, pushTask.getError()); + } + + @Test + public void testSendInBackgroundWithCallbackFail() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + push.sendInBackground(new SendCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + + // Make sure controller is executed and state parameter is correct + assertSame(exception, exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + + @Test + public void testSendSuccess() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + push.send(); + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + + @Test + public void testSendFail() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forError(exception)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParsePush push = new ParsePush(); + JSONObject data = new JSONObject(); + data.put("key", "value"); + List channels = new ArrayList<>(); + channels.add("test"); + channels.add("testAgain"); + push.builder.expirationTime((long)1000) + .data(data) + .pushToIOS(true) + .channelSet(channels); + try { + push.send(); + } catch (ParseException e) { + assertSame(exception, e); + } + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + assertTrue(state.pushToIOS()); + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(2, state.channelSet().size()); + assertTrue(state.channelSet().contains("test")); + assertTrue(state.channelSet().contains("testAgain")); + } + //endregion + + //region testSendMessageInBackground + + @Test + public void testSendMessageInBackground() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParseQuery query = ParseInstallation.getQuery(); + query.getBuilder() + .whereEqualTo("foo", "bar"); + ParseTaskUtils.wait(ParsePush.sendMessageInBackground("test", query)); + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + // Verify query state + ParseQuery.State queryState = state.queryState(); + JSONObject queryStateJson = queryState.toJSON(PointerEncoder.get()); + assertEquals("bar", queryStateJson.getJSONObject("where").getString("foo")); + // Verify message + assertEquals("test", state.data().getString(ParsePush.KEY_DATA_MESSAGE)); + } + + @Test + public void testSendMessageInBackgroundWithCallback() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + ParseQuery query = ParseInstallation.getQuery(); + query.getBuilder() + .whereEqualTo("foo", "bar"); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.sendMessageInBackground("test", query, new SendCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + + // Make sure controller is executed and state parameter is correct + assertNull(exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + // Verify query state + ParseQuery.State queryState = state.queryState(); + JSONObject queryStateJson = queryState.toJSON(PointerEncoder.get()); + assertEquals("bar", queryStateJson.getJSONObject("where").getString("foo")); + // Verify message + assertEquals("test", state.data().getString(ParsePush.KEY_DATA_MESSAGE)); + } + + //endregion + + //region testSendDataInBackground + + @Test + public void testSendDataInBackground() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + JSONObject data = new JSONObject(); + data.put("key", "value"); + data.put("keyAgain", "valueAgain"); + ParseQuery query = ParseInstallation.getQuery(); + query.getBuilder() + .whereEqualTo("foo", "bar"); + ParsePush.sendDataInBackground(data, query); + + // Make sure controller is executed and state parameter is correct + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + // Verify query state + ParseQuery.State queryState = state.queryState(); + JSONObject queryStateJson = queryState.toJSON(PointerEncoder.get()); + assertEquals("bar", queryStateJson.getJSONObject("where").getString("foo")); + // Verify data + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testSendDataInBackgroundWithCallback() throws Exception { + // Mock controller + ParsePushController controller = mock(ParsePushController.class); + when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerPushController(controller); + + // Make sample ParsePush data and call method + JSONObject data = new JSONObject(); + data.put("key", "value"); + data.put("keyAgain", "valueAgain"); + ParseQuery query = ParseInstallation.getQuery(); + query.getBuilder() + .whereEqualTo("foo", "bar"); + final Semaphore done = new Semaphore(0); + final Capture exceptionCapture = new Capture<>(); + ParsePush.sendDataInBackground(data, query, new SendCallback() { + @Override + public void done(ParseException e) { + exceptionCapture.set(e); + done.release(); + } + }); + + // Make sure controller is executed and state parameter is correct + assertNull(exceptionCapture.get()); + assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + ParsePush.State state = stateCaptor.getValue(); + // Verify query state + ParseQuery.State queryState = state.queryState(); + JSONObject queryStateJson = queryState.toJSON(PointerEncoder.get()); + assertEquals("bar", queryStateJson.getJSONObject("where").getString("foo")); + // Verify data + assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); + } + + //endregion + + // TODO(mengyan): Add testSetEnable after we test PushRouter and PushService +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryStateTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryStateTest.java new file mode 100644 index 0000000..17efa90 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryStateTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseQueryStateTest extends ResetPluginsParseTest { + + @Test + public void testDefaults() { + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + ParseQuery.State state = builder.build(); + + assertEquals("TestObject", state.className()); + assertTrue(state.constraints().isEmpty()); + assertTrue(state.includes().isEmpty()); + assertNull(state.selectedKeys()); + assertEquals(-1, state.limit()); + assertEquals(0, state.skip()); + assertTrue(state.order().isEmpty()); + assertTrue(state.extraOptions().isEmpty()); + + assertFalse(state.isTracingEnabled()); + + assertEquals(ParseQuery.CachePolicy.IGNORE_CACHE, state.cachePolicy()); + assertEquals(Long.MAX_VALUE, state.maxCacheAge()); + + assertFalse(state.isFromLocalDatastore()); + assertNull(state.pinName()); + assertFalse(state.ignoreACLs()); + } + + @Test + public void testClassName() { + ParseQuery.State stateA = new ParseQuery.State.Builder<>("TestObject") + .build(); + assertEquals("TestObject", stateA.className()); + + ParseQuery.State stateB = new ParseQuery.State.Builder<>(ParseUser.class) + .build(); + assertEquals("_User", stateB.className()); + } + + @Test + public void testConstraints() { + ParseQuery.QueryConstraints constraints; + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + + constraints = builder + .whereEqualTo("foo", "bar") + .whereEqualTo("foo", "baz") // Should overwrite since same key + .addCondition("people", "$in", Arrays.asList("stanley")) // Collection + .addCondition("people", "$in", Arrays.asList("grantland")) // Collection (overwrite) + .addCondition("something", "$exists", false) // Object + .build() + .constraints(); + assertEquals(3, constraints.size()); + assertEquals("baz", constraints.get("foo")); + Collection in = ((Collection) ((ParseQuery.KeyConstraints) constraints.get("people")).get("$in")); + assertEquals(1, in.size()); + assertEquals("grantland", new ArrayList<>(in).get(0)); + assertEquals(false, ((ParseQuery.KeyConstraints) constraints.get("something")).get("$exists")); + } + + @Test + public void testConstraintsWithSubqueries() { + //TODO + } + + @Test + public void testParseRelation() { + //TODO whereRelatedTo, redirectClassNameForKey + } + + @Test + public void testOrder() { + ParseQuery.State state; + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + + // Ascending adds + builder.orderByAscending("foo"); + state = builder.build(); + assertEquals(1, state.order().size()); + assertEquals("foo", state.order().get(0)); + + // Descending clears and add + builder.orderByDescending("foo"); + state = builder.build(); + assertEquals(1, state.order().size()); + assertEquals("-foo", state.order().get(0)); + + // Add ascending/descending adds + builder.addAscendingOrder("bar"); + builder.addDescendingOrder("baz"); + state = builder.build(); + assertEquals(3, state.order().size()); + assertEquals("-foo", state.order().get(0)); + assertEquals("bar", state.order().get(1)); + assertEquals("-baz", state.order().get(2)); + + // Ascending clears and adds + builder.orderByAscending("foo"); + state = builder.build(); + assertEquals(1, state.order().size()); + } + + @Test + public void testMisc() { // Include, SelectKeys, Limit, Skip + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + + builder.include("foo").include("bar"); + assertEquals(2, builder.build().includes().size()); + + builder.selectKeys(Arrays.asList("foo")).selectKeys(Arrays.asList("bar", "baz", "qux")); + assertEquals(4, builder.build().selectedKeys().size()); + + builder.setLimit(42); + assertEquals(42, builder.getLimit()); + assertEquals(42, builder.build().limit()); + + builder.setSkip(48); + assertEquals(48, builder.getSkip()); + assertEquals(48, builder.build().skip()); + } + + @Test + public void testTrace() { + assertTrue(new ParseQuery.State.Builder<>("TestObject") + .setTracingEnabled(true) + .build() + .isTracingEnabled()); + } + + @Test + public void testCachePolicy() { + //TODO + } + + //TODO(grantland): Add tests for LDS and throwing for LDS/CachePolicy once we remove OfflineStore + // global t6942994 + + @Test(expected = IllegalStateException.class) + public void testThrowIfNotLDSAndIgnoreACLs() { + new ParseQuery.State.Builder<>("TestObject") + .fromNetwork() + .ignoreACLs() + .build(); + } + + //region Or Tests + + @Test + public void testOr() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObject").whereEqualTo("name", "grantland")); + subqueries.add(new ParseQuery.State.Builder<>("TestObject").whereEqualTo("name", "stanley")); + + ParseQuery.State state = ParseQuery.State.Builder.or(subqueries).build(); + assertEquals("TestObject", state.className()); + ParseQuery.QueryConstraints constraints = state.constraints(); + assertEquals(1, constraints.size()); + @SuppressWarnings("unchecked") + List or = + (List) constraints.get("$or"); + assertEquals(2, or.size()); + assertEquals("grantland", or.get(0).get("name")); + assertEquals("stanley", or.get(1).get("name")); + } + + @Test + public void testOrIsMutable() { + List> subqueries = new ArrayList<>(); + ParseQuery.State.Builder builderA = new ParseQuery.State.Builder<>("TestObject"); + subqueries.add(builderA); + ParseQuery.State.Builder builderB = new ParseQuery.State.Builder<>("TestObject"); + subqueries.add(builderB); + + ParseQuery.State.Builder builder = ParseQuery.State.Builder.or(subqueries); + // Mutate subquery after `or` + builderA.whereEqualTo("name", "grantland"); + + ParseQuery.State state = builder.build(); + ParseQuery.QueryConstraints constraints = state.constraints(); + @SuppressWarnings("unchecked") + List or = + (List) constraints.get("$or"); + assertEquals("grantland", or.get(0).get("name")); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithEmptyList() { + ParseQuery.State.Builder.or(new ArrayList>()).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithDifferentClassName() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB")); + ParseQuery.State.Builder.or(subqueries).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithLimit() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB").setLimit(1)); + ParseQuery.State.Builder.or(subqueries).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithSkip() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB").setSkip(1)); + ParseQuery.State.Builder.or(subqueries).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithOrder() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB").orderByAscending("blah")); + ParseQuery.State.Builder.or(subqueries).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithIncludes() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB").include("blah")); + ParseQuery.State.Builder.or(subqueries).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testOrThrowsWithSelectedKeys() { + List> subqueries = new ArrayList<>(); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectA")); + subqueries.add(new ParseQuery.State.Builder<>("TestObjectB").selectKeys(Arrays.asList("blah"))); + ParseQuery.State.Builder.or(subqueries).build(); + } + + //endregion + + @Test + public void testSubqueryToJSON() throws JSONException { + ParseEncoder encoder = PointerEncoder.get(); + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + + JSONObject json = builder.build().toJSON(encoder); + assertEquals("TestObject", json.getString("className")); + assertEquals("{}", json.getString("where")); + int count = 0; Iterator i = json.keys(); while (i.hasNext()) { i.next(); count++; } + assertEquals(2, count); + + ParseQuery.State.Builder subbuilder = new ParseQuery.State.Builder<>("TestObject"); + + json = builder + .whereEqualTo("foo", "bar") + .whereMatchesQuery("subquery", subbuilder) + .setLimit(12) + .setSkip(34) + .orderByAscending("foo").addDescendingOrder("bar") + .include("name") + .selectKeys(Arrays.asList("name", "blah")) + .setTracingEnabled(true) + .redirectClassNameForKey("what") + .build() + .toJSON(encoder); + assertEquals("TestObject", json.getString("className")); + JSONAssert.assertEquals("{" + + "\"foo\":\"bar\"," + + "\"subquery\":{\"$inQuery\":{\"className\":\"TestObject\",\"where\":{}}}" + + "}", json.getJSONObject("where"), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(12, json.getInt("limit")); + assertEquals(34, json.getInt("skip")); + assertEquals("foo,-bar", json.getString("order")); + assertEquals("name", json.getString("include")); + assertEquals("name,blah", json.getString("fields")); + assertEquals(1, json.getInt("trace")); + assertEquals("what", json.getString("redirectClassNameForKey")); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryTest.java new file mode 100644 index 0000000..f58aa35 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseQueryTest.java @@ -0,0 +1,907 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ParseQueryTest { + + @Before + public void setUp() { + ParseTestUtils.setTestParseUser(); + ParseObject.registerSubclass(ParseUser.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseUser.class); + ParseCorePlugins.getInstance().reset(); + Parse.disableLocalDatastore(); + } + + @Test + public void testConstructors() { + assertEquals("_User", ParseQuery.getQuery(ParseUser.class).getClassName()); + assertEquals("TestObject", ParseQuery.getQuery("TestObject").getClassName()); + assertEquals("_User", new ParseQuery<>(ParseUser.class).getClassName()); + assertEquals("TestObject", new ParseQuery<>("TestObject").getClassName()); + + ParseQuery.State.Builder builder = new ParseQuery.State.Builder<>("TestObject"); + ParseQuery query = new ParseQuery<>(builder); + assertEquals("TestObject", query.getClassName()); + assertSame(builder, query.getBuilder()); + } + + @Test + public void testCopy() throws InterruptedException { + ParseQuery query = new ParseQuery<>("TestObject"); + query.setUser(new ParseUser()); + query.whereEqualTo("foo", "bar"); + ParseQuery.State.Builder builder = query.getBuilder(); + ParseQuery.State state = query.getBuilder().build(); + + ParseQuery queryCopy = new ParseQuery<>(query); + ParseQuery.State.Builder builderCopy = queryCopy.getBuilder(); + ParseQuery.State stateCopy = queryCopy.getBuilder().build(); + + assertNotSame(query, queryCopy); + assertSame(query.getUserAsync(state).getResult(), queryCopy.getUserAsync(stateCopy).getResult()); + + assertNotSame(builder, builderCopy); + assertSame(state.constraints().get("foo"), stateCopy.constraints().get("foo")); + } + + // ParseUser#setUser is for tests only + @Test + public void testSetUser() throws ParseException { + ParseQuery query = ParseQuery.getQuery("TestObject"); + + ParseUser user = new ParseUser(); + query.setUser(user); + assertSame(user, ParseTaskUtils.wait(query.getUserAsync(query.getBuilder().build()))); + + // TODO(grantland): Test that it gets the current user + + Parse.enableLocalDatastore(null); + query.fromLocalDatastore() + .ignoreACLs(); + assertNull(ParseTaskUtils.wait(query.getUserAsync(query.getBuilder().build()))); + } + + @Test + public void testMultipleQueries() throws ParseException { + TestQueryController controller1 = new TestQueryController(); + TestQueryController controller2 = new TestQueryController(); + + TaskCompletionSource tcs1 = new TaskCompletionSource<>(); + TaskCompletionSource tcs2 = new TaskCompletionSource<>(); + controller1.await(tcs1.getTask()); + controller2.await(tcs2.getTask()); + + ParseQuery query = ParseQuery.getQuery("TestObject"); + query.setUser(new ParseUser()); + + ParseCorePlugins.getInstance().registerQueryController(controller1); + query.findInBackground(); + assertTrue(query.isRunning()); + + ParseCorePlugins.getInstance().reset(); + ParseCorePlugins.getInstance().registerQueryController(controller2); + query.countInBackground(); + assertTrue(query.isRunning()); + + // Stop the first operation. + tcs1.setResult(null); + assertTrue(query.isRunning()); + + // Stop the second. + tcs2.setResult(null); + assertFalse(query.isRunning()); + } + + @Test + public void testMultipleQueriesWithInflightChanges() throws ParseException { + Parse.enableLocalDatastore(null); + TestQueryController controller = new TestQueryController(); + TaskCompletionSource tcs = new TaskCompletionSource<>(); + controller.await(tcs.getTask()); + + ParseQuery query = ParseQuery.getQuery("TestObject"); + query.setUser(new ParseUser()); + + ParseCorePlugins.getInstance().registerQueryController(controller); + List> tasks = Arrays.asList( + query.fromNetwork().findInBackground().makeVoid(), + query.fromLocalDatastore().findInBackground().makeVoid(), + query.setLimit(10).findInBackground().makeVoid(), + query.whereEqualTo("key", "value").countInBackground().makeVoid()); + assertTrue(query.isRunning()); + tcs.trySetResult(null); + ParseTaskUtils.wait(Task.whenAll(tasks)); + assertFalse(query.isRunning()); + } + + @Test + public void testCountLimitReset() throws ParseException { + // Mock CacheQueryController + ParseQueryController controller = mock(CacheQueryController.class); + ParseCorePlugins.getInstance().registerQueryController(controller); + when(controller.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class))).thenReturn(Task.forResult(0)); + + final ParseQuery query = ParseQuery.getQuery("TestObject"); + + query.countInBackground(); + assertEquals(-1, query.getLimit()); + } + + @Test + public void testCountWithCallbackLimitReset() throws ParseException { + // Mock CacheQueryController + CacheQueryController controller = mock(CacheQueryController.class); + ParseCorePlugins.getInstance().registerQueryController(controller); + when(controller.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class))).thenReturn(Task.forResult(0)); + + final ParseQuery query = ParseQuery.getQuery("TestObject"); + + query.countInBackground(null); + assertEquals(-1, query.getLimit()); + } + + @Test + public void testCountLimit() throws ParseException { + CacheQueryController controller = mock(CacheQueryController.class); + ParseCorePlugins.getInstance().registerQueryController(controller); + when(controller.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class))).thenReturn(Task.forResult(0)); + + ArgumentCaptor state = ArgumentCaptor.forClass(ParseQuery.State.class); + + final ParseQuery query = ParseQuery.getQuery("TestObject"); + query.countInBackground(); + verify(controller, times(1)).countAsync(state.capture(), any(ParseUser.class), any(Task.class)); + assertEquals(0, state.getValue().limit()); + } + + @Test + public void testCountWithCallbackLimit() throws ParseException { + CacheQueryController controller = mock(CacheQueryController.class); + ParseCorePlugins.getInstance().registerQueryController(controller); + when(controller.countAsync( + any(ParseQuery.State.class), + any(ParseUser.class), + any(Task.class))).thenReturn(Task.forResult(0)); + + ArgumentCaptor state = ArgumentCaptor.forClass(ParseQuery.State.class); + + final ParseQuery query = ParseQuery.getQuery("TestObject"); + query.countInBackground(null); + verify(controller, times(1)).countAsync(state.capture(), any(ParseUser.class), any(Task.class)); + assertEquals(0, state.getValue().limit()); + } + + @Test + public void testIsRunning() throws ParseException { + TestQueryController controller = new TestQueryController(); + ParseCorePlugins.getInstance().registerQueryController(controller); + TaskCompletionSource tcs = new TaskCompletionSource<>(); + controller.await(tcs.getTask()); + + ParseQuery query = ParseQuery.getQuery("TestObject"); + query.setUser(new ParseUser()); + assertFalse(query.isRunning()); + query.findInBackground(); + assertTrue(query.isRunning()); + tcs.setResult(null); + assertFalse(query.isRunning()); + // Run another + tcs = new TaskCompletionSource<>(); + controller.await(tcs.getTask()); + query.findInBackground(); + assertTrue(query.isRunning()); + query.cancel(); + assertFalse(query.isRunning()); + } + + @Test + public void testQueryCancellation() throws ParseException { + TestQueryController controller = new TestQueryController(); + ParseCorePlugins.getInstance().registerQueryController(controller); + + TaskCompletionSource tcs = new TaskCompletionSource(); + controller.await(tcs.getTask()); + + ParseQuery query = ParseQuery.getQuery("TestObject"); + query.setUser(new ParseUser()); + Task task = query.findInBackground().makeVoid(); + + query.cancel(); + tcs.setResult(null); + try { + ParseTaskUtils.wait(task); + } catch (RuntimeException e) { + assertThat(e.getCause(), instanceOf(CancellationException.class)); + } + + // Should succeed + task = query.findInBackground().makeVoid(); + ParseTaskUtils.wait(task); + } + + // TODO(grantland): Add CACHE_THEN_NETWORK tests (find, count, getFirst, get) + + // TODO(grantland): Add cache tests (hasCachedResult, clearCachedResult, clearAllCachedResults) + + // TODO(grantland): Add ParseQuery -> ParseQuery.State.Builder calls + + //region testConditions + + @Test + public void testCachePolicy() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + query.setCachePolicy(ParseQuery.CachePolicy.CACHE_ELSE_NETWORK); + + assertEquals(ParseQuery.CachePolicy.CACHE_ELSE_NETWORK, query.getCachePolicy()); + } + + @Test + public void testFromNetwork() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + Parse.enableLocalDatastore(null); + query.fromNetwork(); + + assertTrue(query.isFromNetwork()); + } + + @Test + public void testFromPin() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + Parse.enableLocalDatastore(null); + query.fromPin(); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertTrue(state.isFromLocalDatastore()); + assertEquals(ParseObject.DEFAULT_PIN, state.pinName()); + } + + @Test + public void testMaxCacheAge() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + query.setMaxCacheAge(10); + + assertEquals(10, query.getMaxCacheAge()); + } + + @Test + public void testWhereNotEqualTo() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereNotEqualTo("key", "value"); + + verifyCondition(query, "key", "$ne", "value"); + } + + @Test + public void testWhereGreaterThan() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereGreaterThan("key", "value"); + + verifyCondition(query, "key", "$gt", "value"); + } + + @Test + public void testWhereLessThanOrEqualTo() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereLessThanOrEqualTo("key", "value"); + + verifyCondition(query, "key", "$lte", "value"); + } + + @Test + public void testWhereGreaterThanOrEqualTo() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereGreaterThanOrEqualTo("key", "value"); + + verifyCondition(query, "key", "$gte", "value"); + } + + @Test + public void testWhereContainedIn() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + List values = Arrays.asList("value", "valueAgain"); + + query.whereContainedIn("key", values); + + verifyCondition(query, "key", "$in", values); + } + + @Test + public void testWhereFullText() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + String text = "TestString"; + query.whereFullText("key", text); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = (ParseQuery.KeyConstraints) queryConstraints.get("key"); + Map searchDictionary = (Map) keyConstraints.get("$text"); + Map termDictionary = (Map) searchDictionary.get("$search"); + String value = (String) termDictionary.get("$term"); + assertEquals(value, text); + } + + @Test + public void testWhereContainsAll() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + List values = Arrays.asList("value", "valueAgain"); + + query.whereContainsAll("key", values); + + verifyCondition(query, "key", "$all", values); + } + + @Test + public void testWhereNotContainedIn() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + List values = Arrays.asList("value", "valueAgain"); + + query.whereNotContainedIn("key", values); + + verifyCondition(query, "key", "$nin", values); + } + + @Test + public void testWhereMatches() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereMatches("key", "regex"); + + verifyCondition(query, "key", "$regex", "regex"); + } + + @Test + public void testWhereMatchesWithModifiers() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereMatches("key", "regex", "modifiers"); + + verifyCondition(query, "key", "$regex", "regex"); + verifyCondition(query, "key", "$options", "modifiers"); + } + + @Test + public void testWhereStartsWith() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + String value = "prefix"; + query.whereStartsWith("key", value); + + verifyCondition(query, "key", "$regex", "^" + Pattern.quote(value)); + } + + @Test + public void testWhereEndsWith() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + String value = "suffix"; + query.whereEndsWith("key", value); + + verifyCondition(query, "key", "$regex", Pattern.quote(value) + "$"); + } + + @Test + public void testWhereExists() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereExists("key"); + + verifyCondition(query, "key", "$exists", true); + } + + @Test + public void testWhereDoesNotExist() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.whereDoesNotExist("key"); + + verifyCondition(query, "key", "$exists", false); + } + + @Test + public void testWhereContains() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + String value = "value"; + query.whereContains("key", value); + + verifyCondition(query, "key", "$regex", Pattern.quote(value)); + } + + @Test + public void testWhereMatchesQuery() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseQuery conditionQuery = new ParseQuery<>("Test"); + conditionQuery.whereExists("keyAgain"); + + query.whereMatchesQuery("key", conditionQuery); + + verifyCondition(query, "key", "$inQuery", conditionQuery.getBuilder()); + } + + @Test + public void testWhereDoesNotMatchQuery() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseQuery conditionQuery = new ParseQuery<>("Test"); + conditionQuery.whereExists("keyAgain"); + + query.whereDoesNotMatchQuery("key", conditionQuery); + + verifyCondition(query, "key", "$notInQuery", conditionQuery.getBuilder()); + } + + @Test + public void testWhereMatchesKeyInQuery() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseQuery conditionQuery = new ParseQuery<>("Test"); + conditionQuery.whereExists("keyAgain"); + + query.whereMatchesKeyInQuery("key", "keyAgain", conditionQuery); + + Map conditions = new HashMap<>(); + conditions.put("key", "keyAgain"); + conditions.put("query", conditionQuery.getBuilder()); + verifyCondition(query, "key", "$select", conditions); + } + + @Test + public void testWhereDoesNotMatchKeyInQuery() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseQuery conditionQuery = new ParseQuery<>("Test"); + conditionQuery.whereExists("keyAgain"); + + query.whereDoesNotMatchKeyInQuery("key", "keyAgain", conditionQuery); + + Map conditions = new HashMap<>(); + conditions.put("key", "keyAgain"); + conditions.put("query", conditionQuery.getBuilder()); + verifyCondition(query, "key", "$dontSelect", conditions); + } + + @Test + public void testWhereNear() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + + query.whereNear("key", point); + + verifyCondition(query, "key", "$nearSphere", point); + } + + @Test + public void testWhereWithinGeoBox() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + ParseGeoPoint pointAgain = new ParseGeoPoint(20, 20); + + query.whereWithinGeoBox("key", point, pointAgain); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = + (ParseQuery.KeyConstraints) queryConstraints.get("key"); + Map map = (Map) keyConstraints.get("$within"); + List list = (List) map.get("$box"); + assertEquals(2, list.size()); + assertTrue(list.contains(point)); + assertTrue(list.contains(pointAgain)); + } + + @Test + public void testWhereWithinPolygon() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point1 = new ParseGeoPoint(10, 10); + ParseGeoPoint point2 = new ParseGeoPoint(20, 20); + ParseGeoPoint point3 = new ParseGeoPoint(30, 30); + + List points = Arrays.asList(point1, point2, point3); + query.whereWithinPolygon("key", points); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = (ParseQuery.KeyConstraints) queryConstraints.get("key"); + Map map = (Map) keyConstraints.get("$geoWithin"); + List list = (List) map.get("$polygon"); + assertEquals(3, list.size()); + assertTrue(list.contains(point1)); + assertTrue(list.contains(point2)); + assertTrue(list.contains(point3)); + } + + @Test + public void testWhereWithinPolygonWithPolygon() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point1 = new ParseGeoPoint(10, 10); + ParseGeoPoint point2 = new ParseGeoPoint(20, 20); + ParseGeoPoint point3 = new ParseGeoPoint(30, 30); + + List points = Arrays.asList(point1, point2, point3); + query.whereWithinPolygon("key", new ParsePolygon(points)); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = (ParseQuery.KeyConstraints) queryConstraints.get("key"); + Map map = (Map) keyConstraints.get("$geoWithin"); + List list = (List) map.get("$polygon"); + assertEquals(3, list.size()); + assertTrue(list.contains(point1)); + assertTrue(list.contains(point2)); + assertTrue(list.contains(point3)); + } + + @Test + public void testWherePolygonContains() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + + query.wherePolygonContains("key", point); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = + (ParseQuery.KeyConstraints) queryConstraints.get("key"); + Map map = (Map) keyConstraints.get("$geoIntersects"); + ParseGeoPoint geoPoint = (ParseGeoPoint) map.get("$point"); + assertEquals(geoPoint, point); + } + + @Test + public void testWhereWithinRadians() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + + query.whereWithinRadians("key", point, 100.0); + + verifyCondition(query, "key", "$nearSphere", point); + verifyCondition(query, "key", "$maxDistance", 100.0); + } + + @Test + public void testWhereWithinMiles() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + + query.whereWithinMiles("key", point, 100.0); + + verifyCondition(query, "key", "$nearSphere", point); + verifyCondition(query, "key", "$maxDistance", 100.0 / ParseGeoPoint.EARTH_MEAN_RADIUS_MILE); + } + + @Test + public void testWhereWithinKilometers() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + ParseGeoPoint point = new ParseGeoPoint(10, 10); + + query.whereWithinKilometers("key", point, 100.0); + + verifyCondition(query, "key", "$nearSphere", point); + verifyCondition(query, "key", "$maxDistance", 100.0 / ParseGeoPoint.EARTH_MEAN_RADIUS_KM); + } + + @Test + public void testClear() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + query.whereEqualTo("key", "value"); + query.whereEqualTo("otherKey", "otherValue"); + verifyCondition(query, "key", "value"); + verifyCondition(query, "otherKey", "otherValue"); + query.clear("key"); + verifyCondition(query, "key", null); + verifyCondition(query, "otherKey", "otherValue"); // still. + } + + @Test + public void testOr() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + query.whereEqualTo("key", "value"); + ParseQuery queryAgain = new ParseQuery<>("Test"); + queryAgain.whereEqualTo("keyAgain", "valueAgain"); + List> queries = Arrays.asList(query, queryAgain); + ParseQuery combinedQuery = ParseQuery.or(queries); + + // We generate a state to verify the content of the builder + ParseQuery.State state = combinedQuery.getBuilder().build(); + ParseQuery.QueryConstraints combinedQueryConstraints = state.constraints(); + List list = (List) combinedQueryConstraints.get("$or"); + assertEquals(2, list.size()); + // Verify query constraint + ParseQuery.QueryConstraints queryConstraintsFromCombinedQuery = + (ParseQuery.QueryConstraints) list.get(0); + assertEquals(1, queryConstraintsFromCombinedQuery.size()); + assertEquals("value", queryConstraintsFromCombinedQuery.get("key")); + // Verify queryAgain constraint + ParseQuery.QueryConstraints queryAgainConstraintsFromCombinedQuery = + (ParseQuery.QueryConstraints) list.get(1); + assertEquals(1, queryAgainConstraintsFromCombinedQuery.size()); + assertEquals("valueAgain", queryAgainConstraintsFromCombinedQuery.get("keyAgain")); + } + + // TODO(mengyan): Add testOr illegal cases unit test + + @Test + public void testInclude() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.include("key"); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(1, state.includes().size()); + assertTrue(state.includes().contains("key")); + } + + @Test + public void testSelectKeys() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.selectKeys(Arrays.asList("key", "keyAgain")); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(2, state.selectedKeys().size()); + assertTrue(state.selectedKeys().contains("key")); + assertTrue(state.selectedKeys().contains("keyAgain")); + } + + @Test + public void testAddAscendingOrder() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.addAscendingOrder("key"); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(1, state.order().size()); + assertTrue(state.order().contains("key")); + } + + @Test + public void testAddDescendingOrder() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.addDescendingOrder("key"); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(1, state.order().size()); + assertTrue(state.order().contains(String.format("-%s", "key"))); + } + + @Test + public void testOrderByAscending() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.orderByAscending("key"); + query.orderByAscending("keyAgain"); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(1, state.order().size()); + assertTrue(state.order().contains("keyAgain")); + } + + @Test + public void testOrderByDescending() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.orderByDescending("key"); + query.orderByDescending("keyAgain"); + + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + assertEquals(1, state.order().size()); + assertTrue(state.order().contains(String.format("-%s", "keyAgain"))); + } + + @Test + public void testLimit() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.setLimit(5); + + assertEquals(5, query.getLimit()); + } + + @Test + public void testSkip() throws Exception { + ParseQuery query = new ParseQuery<>("Test"); + + query.setSkip(5); + + assertEquals(5, query.getSkip()); + } + + private static void verifyCondition(ParseQuery query, String key, Object value) { + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + assertEquals(value, queryConstraints.get(key)); + } + + private static void verifyCondition( + ParseQuery query, String key, String conditionKey, Object value) { + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = + (ParseQuery.KeyConstraints) queryConstraints.get(key); + assertEquals(value, keyConstraints.get(conditionKey)); + } + + private static void verifyCondition( + ParseQuery query, String key, String conditionKey, List values) { + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = + (ParseQuery.KeyConstraints) queryConstraints.get(key); + Collection list = (Collection) keyConstraints.get(conditionKey); + assertEquals(values.size(), list.size()); + for (Object value : values) { + assertTrue(list.contains(value)); + } + } + + private static void verifyCondition( + ParseQuery query, String key, String conditionKey, Map values) { + // We generate a state to verify the content of the builder + ParseQuery.State state = query.getBuilder().build(); + ParseQuery.QueryConstraints queryConstraints = state.constraints(); + ParseQuery.KeyConstraints keyConstraints = + (ParseQuery.KeyConstraints) queryConstraints.get(key); + Map map = (Map) keyConstraints.get(conditionKey); + assertEquals(values.size(), map.size()); + for (Object constraintKey : map.keySet()) { + assertTrue(values.containsKey(constraintKey)); + assertEquals(map.get(constraintKey), values.get(constraintKey)); + } + } + + //endregion + + /** + * A {@link ParseQueryController} used for testing. + */ + private static class TestQueryController implements ParseQueryController { + + private Task toAwait = Task.forResult(null); + + public Task await(final Task task) { + toAwait = toAwait.continueWithTask(new Continuation>() { + @Override + public Task then(Task ignored) throws Exception { + return task; + } + }); + return toAwait; + } + + @Override + public Task> findAsync(ParseQuery.State state, + ParseUser user, Task cancellationToken) { + final AtomicBoolean cancelled = new AtomicBoolean(false); + cancellationToken.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + cancelled.set(true); + return null; + } + }); + return await(Task.forResult(null).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (cancelled.get()) { + return Task.cancelled(); + } + return task; + } + })).cast(); + } + + @Override + public Task countAsync(ParseQuery.State state, + ParseUser user, Task cancellationToken) { + final AtomicBoolean cancelled = new AtomicBoolean(false); + cancellationToken.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + cancelled.set(true); + return null; + } + }); + return await(Task.forResult(null).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (cancelled.get()) { + return Task.cancelled(); + } + return task; + } + })).cast(); + } + + @Override + public Task getFirstAsync(ParseQuery.State state, + ParseUser user, Task cancellationToken) { + final AtomicBoolean cancelled = new AtomicBoolean(false); + cancellationToken.continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + cancelled.set(true); + return null; + } + }); + return await(Task.forResult(null).continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (cancelled.get()) { + return Task.cancelled(); + } + return task; + } + })).cast(); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTCommandTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTCommandTest.java new file mode 100644 index 0000000..333ae0a --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTCommandTest.java @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +// For org.json +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class ParseRESTCommandTest { + + private static ParseHttpResponse newMockParseHttpResponse(int statusCode, JSONObject body) { + return newMockParseHttpResponse(statusCode, body.toString()); + } + + private static ParseHttpResponse newMockParseHttpResponse(int statusCode, String body) { + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(statusCode) + .setTotalSize((long) body.length()) + .setContent(new ByteArrayInputStream(body.getBytes())) + .build(); + return mockResponse; + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + ParseRequest.setDefaultInitialRetryDelay(1L); + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() throws Exception { + ParseRequest.setDefaultInitialRetryDelay(ParseRequest.DEFAULT_INITIAL_RETRY_DELAY); + ParseCorePlugins.getInstance().reset(); + ParseRESTCommand.server = null; + } + + + @Test + public void testInitializationWithDefaultParseServerURL() throws Exception { + ParseRESTCommand.server = new URL("https://api.parse.com/1/"); + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath("events/Appopened") + .build(); + + assertEquals("https://api.parse.com/1/events/Appopened", command.url); + } + + @Test + public void testPermanentFailures() throws Exception { + JSONObject json = new JSONObject(); + json.put("code", 1337); + json.put("error", "mock error"); + + ParseHttpResponse response = newMockParseHttpResponse(400, json); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .method(ParseHttpRequest.Method.GET) + .installationId("fake_installation_id") + .build(); + Task task = command.executeAsync(client); + task.waitForCompletion(); + verify(client, times(1)).execute(any(ParseHttpRequest.class)); + + assertTrue(task.isFaulted()); + assertEquals(1337, ((ParseException) task.getError()).getCode()); + assertEquals("mock error", task.getError().getMessage()); + } + + @Test + public void testTemporaryFailures() throws Exception { + JSONObject json = new JSONObject(); + json.put("code", 1337); + json.put("error", "mock error"); + + ParseHttpResponse response1 = newMockParseHttpResponse(500, json); + ParseHttpResponse response2 = newMockParseHttpResponse(500, json); + ParseHttpResponse response3 = newMockParseHttpResponse(500, json); + ParseHttpResponse response4 = newMockParseHttpResponse(500, json); + ParseHttpResponse response5 = newMockParseHttpResponse(500, json); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn( + response1, + response2, + response3, + response4, + response5 + ); + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .method(ParseHttpRequest.Method.GET) + .installationId("fake_installation_id") + .build(); + Task task = command.executeAsync(client); + task.waitForCompletion(); + verify(client, times(5)).execute(any(ParseHttpRequest.class)); + + assertTrue(task.isFaulted()); + assertEquals(1337, ((ParseException) task.getError()).getCode()); + assertEquals("mock error", task.getError().getMessage()); + } + + /** + * Test to verify that handle 401 unauthorized + */ + @Test + public void test401Unauthorized() throws Exception { + JSONObject json = new JSONObject(); + json.put("error", "unauthorized"); + + ParseHttpResponse response = newMockParseHttpResponse(401, json); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .method(ParseHttpRequest.Method.GET) + .installationId("fake_installation_id") + .build(); + Task task = command.executeAsync(client); + task.waitForCompletion(); + verify(client, times(1)).execute(any(ParseHttpRequest.class)); + + assertTrue(task.isFaulted()); + assertEquals(0, ((ParseException) task.getError()).getCode()); + assertEquals("unauthorized", task.getError().getMessage()); + } + + @Test + public void testToDeterministicString() throws Exception { + // Make test json + JSONArray nestedJSONArray = new JSONArray() + .put(true) + .put(1) + .put("test"); + JSONObject nestedJSON = new JSONObject() + .put("bool", false) + .put("int", 2) + .put("string", "test"); + JSONObject json = new JSONObject() + .put("json", nestedJSON) + .put("jsonArray", nestedJSONArray) + .put("bool", true) + .put("int", 3) + .put("string", "test"); + + String jsonString = ParseRESTCommand.toDeterministicString(json); + + JSONObject jsonAgain = new JSONObject(jsonString); + assertEquals(json, jsonAgain, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testToJSONObject() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + JSONObject jsonParameters = new JSONObject() + .put("count", 1) + .put("limit", 1); + String sessionToken = "sessionToken"; + String localId = "localId"; + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + JSONObject json = command.toJSONObject(); + + assertEquals(httpPath, json.getString("httpPath")); + assertEquals("POST", json.getString("httpMethod")); + assertEquals(jsonParameters, json.getJSONObject("parameters"), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(sessionToken, json.getString("sessionToken")); + assertEquals(localId, json.getString("localId")); + } + + @Test + public void testGetCacheKey() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + JSONObject jsonParameters = new JSONObject() + .put("count", 1) + .put("limit", 1); + String sessionToken = "sessionToken"; + String localId = "localId"; + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + String cacheKey = command.getCacheKey(); + + assertTrue(cacheKey.contains("ParseRESTCommand")); + assertTrue(cacheKey.contains(ParseHttpRequest.Method.POST.toString())); + assertTrue(cacheKey.contains(ParseDigestUtils.md5(httpPath))); + String str = + ParseDigestUtils.md5(ParseRESTCommand.toDeterministicString(jsonParameters) + sessionToken); + assertTrue(cacheKey.contains(str)); + } + + @Test + public void testGetCacheKeyWithNoJSONParameters() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + String sessionToken = "sessionToken"; + String localId = "localId"; + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + String cacheKey = command.getCacheKey(); + + assertTrue(cacheKey.contains("ParseRESTCommand")); + assertTrue(cacheKey.contains(ParseHttpRequest.Method.POST.toString())); + assertTrue(cacheKey.contains(ParseDigestUtils.md5(httpPath))); + assertTrue(cacheKey.contains(ParseDigestUtils.md5(sessionToken))); + } + + @Test + public void testReleaseLocalIds() { + // Register LocalIdManager + LocalIdManager localIdManager = mock(LocalIdManager.class); + when(localIdManager.createLocalId()).thenReturn("localIdAgain"); + ParseCorePlugins.getInstance().registerLocalIdManager(localIdManager); + + // Make test command + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + String httpPath = "www.parse.com"; + JSONObject jsonParameters = PointerOrLocalIdEncoder.get().encodeRelatedObject(object); + String sessionToken = "sessionToken"; + String localId = "localId"; + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + command.releaseLocalIds(); + + verify(localIdManager, times(1)).releaseLocalIdOnDisk(localId); + verify(localIdManager, times(1)).releaseLocalIdOnDisk("localIdAgain"); + } + + @Test + public void testResolveLocalIdsWithNoObjectId() { + // Register LocalIdManager + LocalIdManager localIdManager = mock(LocalIdManager.class); + when(localIdManager.createLocalId()).thenReturn("localIdAgain"); + ParseCorePlugins.getInstance().registerLocalIdManager(localIdManager); + + // Make test command + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + String httpPath = "www.parse.com"; + JSONObject jsonParameters = PointerOrLocalIdEncoder.get().encodeRelatedObject(object); + String sessionToken = "sessionToken"; + String localId = "localId"; + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Tried to serialize a command referencing a new, unsaved object."); + command.resolveLocalIds(); + + // Make sure we try to get the objectId + verify(localIdManager, times(1)).getObjectId("localIdAgain"); + } + + @Test + public void testResolveLocalIds() throws Exception { + // Register LocalIdManager + LocalIdManager localIdManager = mock(LocalIdManager.class); + when(localIdManager.createLocalId()).thenReturn("localIdAgain"); + when(localIdManager.getObjectId("localIdAgain")).thenReturn("objectIdAgain"); + when(localIdManager.getObjectId("localId")).thenReturn("objectId"); + ParseCorePlugins.getInstance().registerLocalIdManager(localIdManager); + + // Make test command + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + String httpPath = "classes"; + JSONObject jsonParameters = PointerOrLocalIdEncoder.get().encodeRelatedObject(object); + String sessionToken = "sessionToken"; + String localId = "localId"; + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + command.resolveLocalIds(); + + verify(localIdManager, times(1)).getObjectId("localIdAgain"); + verify(localIdManager, times(1)).getObjectId("localId"); + // Make sure localId in jsonParameters has been replaced with objectId + assertFalse(jsonParameters.has("localId")); + assertEquals("objectIdAgain", jsonParameters.getString("objectId")); + // Make sure localId in command has been replaced with objectId + assertNull(command.getLocalId()); + // Make sure httpMethod has been changed + assertEquals(ParseHttpRequest.Method.PUT, command.method); + // Make sure objectId has been added to httpPath + assertTrue(command.httpPath.contains("objectId")); + } + + @Test + public void testRetainLocalIds() throws Exception { + // Register LocalIdManager + LocalIdManager localIdManager = mock(LocalIdManager.class); + when(localIdManager.createLocalId()).thenReturn("localIdAgain"); + ParseCorePlugins.getInstance().registerLocalIdManager(localIdManager); + + // Make test command + ParseObject object = new ParseObject("Test"); + object.put("key", "value"); + String httpPath = "classes"; + JSONObject jsonParameters = PointerOrLocalIdEncoder.get().encodeRelatedObject(object); + String sessionToken = "sessionToken"; + String localId = "localId"; + + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.POST) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + command.retainLocalIds(); + + verify(localIdManager, times(1)).retainLocalIdOnDisk("localIdAgain"); + verify(localIdManager, times(1)).retainLocalIdOnDisk(localId); + } + + @Test + public void testNewBodyWithNoJSONParameters() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + String sessionToken = "sessionToken"; + String localId = "localId"; + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .method(ParseHttpRequest.Method.GET) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + thrown.expect(IllegalArgumentException.class); + String message = String.format("Trying to execute a %s command without body parameters.", + ParseHttpRequest.Method.GET.toString()); + thrown.expectMessage(message); + + command.newBody(null); + } + + @Test + public void testNewBody() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + JSONObject jsonParameters = new JSONObject() + .put("count", 1) + .put("limit", 1); + String sessionToken = "sessionToken"; + String localId = "localId"; + ParseRESTCommand command = new ParseRESTCommand.Builder() + .httpPath(httpPath) + .jsonParameters(jsonParameters) + .method(ParseHttpRequest.Method.GET) + .sessionToken(sessionToken) + .localId(localId) + .build(); + + ParseByteArrayHttpBody body = (ParseByteArrayHttpBody) command.newBody(null); + + // Verify body content is correct + JSONObject json = new JSONObject(new String(ParseIOUtils.toByteArray(body.getContent()))); + assertEquals(1, json.getInt("count")); + assertEquals(1, json.getInt("limit")); + assertEquals(ParseHttpRequest.Method.GET.toString(), json.getString("_method")); + // Verify body content-type is correct + assertEquals("application/json", body.getContentType()); + } + + @Test + public void testFromJSONObject() throws Exception { + // Make test command + String httpPath = "www.parse.com"; + JSONObject jsonParameters = new JSONObject() + .put("count", 1) + .put("limit", 1); + String sessionToken = "sessionToken"; + String localId = "localId"; + String httpMethod = "POST"; + JSONObject commandJSON = new JSONObject() + .put("httpPath", httpPath) + .put("parameters", jsonParameters) + .put("httpMethod", httpMethod) + .put("sessionToken", sessionToken) + .put("localId", localId); + + ParseRESTCommand command = ParseRESTCommand.fromJSONObject(commandJSON); + + assertEquals(httpPath, command.httpPath); + assertEquals(httpMethod, command.method.toString()); + assertEquals(sessionToken, command.getSessionToken()); + assertEquals(localId, command.getLocalId()); + assertEquals(jsonParameters, command.jsonParameters, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testOnResponseCloseNetworkStreamWithNormalResponse() throws Exception { + // Mock response stream + int statusCode = 200; + JSONObject bodyJson = new JSONObject(); + bodyJson.put("key", "value"); + String bodyStr = bodyJson.toString(); + ByteArrayInputStream bodyStream = new ByteArrayInputStream(bodyStr.getBytes()); + InputStream mockResponseStream = spy(bodyStream); + doNothing() + .when(mockResponseStream) + .close(); + // Mock response + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(statusCode) + .setTotalSize((long) bodyStr.length()) + .setContent(mockResponseStream) + .build(); + + ParseRESTCommand command = new ParseRESTCommand.Builder().build(); + JSONObject json = ParseTaskUtils.wait(command.onResponseAsync(mockResponse, null)); + + verify(mockResponseStream, times(1)).close(); + assertEquals(bodyJson, json, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testOnResposneCloseNetworkStreamWithIOException() throws Exception { + // Mock response stream + int statusCode = 200; + InputStream mockResponseStream = mock(InputStream.class); + doNothing() + .when(mockResponseStream) + .close(); + IOException readException = new IOException("Error"); + doThrow(readException) + .when(mockResponseStream) + .read(); + doThrow(readException) + .when(mockResponseStream) + .read(any(byte[].class)); + // Mock response + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(statusCode) + .setContent(mockResponseStream) + .build(); + + ParseRESTCommand command = new ParseRESTCommand.Builder().build(); + // We can not use ParseTaskUtils here since it will replace the original exception with runtime + // exception + Task responseTask = command.onResponseAsync(mockResponse, null); + responseTask.waitForCompletion(); + + assertTrue(responseTask.isFaulted()); + assertTrue(responseTask.getError() instanceof IOException); + assertEquals("Error", responseTask.getError().getMessage()); + verify(mockResponseStream, times(1)).close(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTQueryCommandTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTQueryCommandTest.java new file mode 100644 index 0000000..95f88dd --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTQueryCommandTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +public class ParseRESTQueryCommandTest { + + @Before + public void setUp() throws MalformedURLException { + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseRESTCommand.server = null; + } + + //region testEncode + + @Test + public void testEncodeWithNoCount() throws Exception { + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .orderByAscending("orderKey") + .addCondition("inKey", "$in", Arrays.asList("inValue", "inValueAgain")) + .selectKeys(Arrays.asList("selectedKey, selectedKeyAgain")) + .include("includeKey") + .setLimit(5) + .setSkip(6) + .redirectClassNameForKey("extraKey") + .setTracingEnabled(true) + .build(); + + Map encoded = ParseRESTQueryCommand.encode(state, false); + + assertEquals("orderKey", encoded.get(ParseRESTQueryCommand.KEY_ORDER)); + JSONObject conditionJson = new JSONObject(encoded.get(ParseRESTQueryCommand.KEY_WHERE)); + JSONArray conditionWhereJsonArray = new JSONArray() + .put("inValue") + .put("inValueAgain"); + assertEquals( + conditionWhereJsonArray, + conditionJson.getJSONObject("inKey").getJSONArray("$in"), + JSONCompareMode.NON_EXTENSIBLE); + assertTrue(encoded.get(ParseRESTQueryCommand.KEY_KEYS).contains("selectedKey")); + assertTrue(encoded.get(ParseRESTQueryCommand.KEY_KEYS).contains("selectedKeyAgain")); + assertEquals("includeKey", encoded.get(ParseRESTQueryCommand.KEY_INCLUDE)); + assertEquals("5", encoded.get(ParseRESTQueryCommand.KEY_LIMIT)); + assertEquals("6", encoded.get(ParseRESTQueryCommand.KEY_SKIP)); + assertEquals("extraKey", encoded.get("redirectClassNameForKey")); + assertEquals("1", encoded.get(ParseRESTQueryCommand.KEY_TRACE)); + } + + @Test + public void testEncodeWithCount() throws Exception { + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .setSkip(6) + .setLimit(3) + .build(); + + Map encoded = ParseRESTQueryCommand.encode(state, true); + + // Limit should not be stripped out from count queries + assertTrue(encoded.containsKey(ParseRESTQueryCommand.KEY_LIMIT)); + assertFalse(encoded.containsKey(ParseRESTQueryCommand.KEY_SKIP)); + assertEquals("1", encoded.get(ParseRESTQueryCommand.KEY_COUNT)); + } + + //endregion + + //region testConstruct + + @Test + public void testFindCommand() throws Exception { + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .selectKeys(Arrays.asList("key", "kayAgain")) + .build(); + + ParseRESTQueryCommand command = ParseRESTQueryCommand.findCommand(state, "sessionToken"); + + assertEquals("classes/TestObject", command.httpPath); + assertEquals(ParseHttpRequest.Method.GET, command.method); + assertEquals("sessionToken", command.getSessionToken()); + Map parameters = ParseRESTQueryCommand.encode(state, false); + JSONObject jsonParameters = (JSONObject) NoObjectsEncoder.get().encode(parameters); + assertEquals(jsonParameters, command.jsonParameters, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void testCountCommand() throws Exception { + ParseQuery.State state = new ParseQuery.State.Builder<>("TestObject") + .selectKeys(Arrays.asList("key", "kayAgain")) + .build(); + + ParseRESTQueryCommand command = ParseRESTQueryCommand.countCommand(state, "sessionToken"); + + assertEquals("classes/TestObject", command.httpPath); + assertEquals(ParseHttpRequest.Method.GET, command.method); + assertEquals("sessionToken", command.getSessionToken()); + Map parameters = ParseRESTQueryCommand.encode(state, true); + JSONObject jsonParameters = (JSONObject) NoObjectsEncoder.get().encode(parameters); + assertEquals(jsonParameters, command.jsonParameters, JSONCompareMode.NON_EXTENSIBLE); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTUserCommandTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTUserCommandTest.java new file mode 100644 index 0000000..5a60093 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRESTUserCommandTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.ByteArrayInputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +public class ParseRESTUserCommandTest { + + @Before + public void setUp() throws MalformedURLException { + ParseObject.registerSubclass(ParseUser.class); + ParseRESTCommand.server = new URL("https://api.parse.com/1"); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseUser.class); + ParseRESTCommand.server = null; + } + + //region testConstruct + + @Test + public void testGetCurrentUserCommand() throws Exception { + ParseRESTUserCommand command = ParseRESTUserCommand.getCurrentUserCommand("sessionToken"); + + assertEquals("users/me", command.httpPath); + assertEquals(ParseHttpRequest.Method.GET, command.method); + assertNull(command.jsonParameters); + assertEquals("sessionToken", command.getSessionToken()); + // TODO(mengyan): Find a way to verify revocableSession + } + + @Test + public void testLogInUserCommand() throws Exception { + ParseRESTUserCommand command = ParseRESTUserCommand.logInUserCommand( + "userName", "password", true); + + assertEquals("login", command.httpPath); + assertEquals(ParseHttpRequest.Method.GET, command.method); + assertEquals("userName", command.jsonParameters.getString("username")); + assertEquals("password", command.jsonParameters.getString("password")); + assertNull(command.getSessionToken()); + // TODO(mengyan): Find a way to verify revocableSession + } + + @Test + public void testResetPasswordResetCommand() throws Exception { + ParseRESTUserCommand command = ParseRESTUserCommand.resetPasswordResetCommand("test@parse.com"); + + assertEquals("requestPasswordReset", command.httpPath); + assertEquals(ParseHttpRequest.Method.POST, command.method); + assertEquals("test@parse.com", command.jsonParameters.getString("email")); + assertNull(command.getSessionToken()); + // TODO(mengyan): Find a way to verify revocableSession + } + + @Test + public void testSignUpUserCommand() throws Exception { + JSONObject parameters = new JSONObject(); + parameters.put("key", "value"); + ParseRESTUserCommand command = + ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true); + + assertEquals("users", command.httpPath); + assertEquals(ParseHttpRequest.Method.POST, command.method); + assertEquals("value", command.jsonParameters.getString("key")); + assertEquals("sessionToken", command.getSessionToken()); + // TODO(mengyan): Find a way to verify revocableSession + } + + @Test + public void testServiceLogInUserCommandWithParameters() throws Exception { + JSONObject parameters = new JSONObject(); + parameters.put("key", "value"); + ParseRESTUserCommand command = + ParseRESTUserCommand.serviceLogInUserCommand(parameters, "sessionToken", true); + + assertEquals("users", command.httpPath); + assertEquals(ParseHttpRequest.Method.POST, command.method); + assertEquals("value", command.jsonParameters.getString("key")); + assertEquals("sessionToken", command.getSessionToken()); + // TODO(mengyan): Find a way to verify revocableSession + } + + @Test + public void testServiceLogInUserCommandWithAuthType() throws Exception { + Map facebookAuthData = new HashMap<>(); + facebookAuthData.put("token", "test"); + ParseRESTUserCommand command = + ParseRESTUserCommand.serviceLogInUserCommand("facebook", facebookAuthData, true); + + assertEquals("users", command.httpPath); + assertEquals(ParseHttpRequest.Method.POST, command.method); + assertNull(command.getSessionToken()); + JSONObject authenticationData = new JSONObject(); + authenticationData.put("facebook", PointerEncoder.get().encode(facebookAuthData)); + JSONObject parameters = new JSONObject(); + parameters.put("authData", authenticationData); + assertEquals(parameters, command.jsonParameters, JSONCompareMode.NON_EXTENSIBLE); + // TODO(mengyan): Find a way to verify revocableSession + } + + //endregion + + //region testAddAdditionalHeaders + + @Test + public void testAddAdditionalHeaders() throws Exception { + JSONObject parameters = new JSONObject(); + parameters.put("key", "value"); + ParseRESTUserCommand command = + ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true); + + ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder(); + command.addAdditionalHeaders(requestBuilder); + + assertEquals("1", requestBuilder.build().getHeader("X-Parse-Revocable-Session")); + } + + //endregion + + //region testOnResponseAsync + + @Test + public void testOnResponseAsync() throws Exception { + ParseRESTUserCommand command = + ParseRESTUserCommand.getCurrentUserCommand("sessionToken"); + + String content = "content"; + String contentType = "application/json"; + int statusCode = 200; + + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(content.getBytes())) + .setContentType(contentType) + .setStatusCode(statusCode) + .build(); + command.onResponseAsync(response, null); + + assertEquals(200, command.getStatusCode()); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRelationTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRelationTest.java new file mode 100644 index 0000000..11fb7b0 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRelationTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseRelationTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + //region testConstructor + + @Test + public void testConstructorWithParentAndKey() { + ParseObject parent = mock(ParseObject.class); + String key = "test"; + + ParseRelation relation = new ParseRelation(parent, key); + + assertEquals(key, relation.getKey()); + assertSame(parent, relation.getParent()); + assertNull(relation.getTargetClass()); + } + + @Test + public void testConstructorWithTargetClass() { + String targetClass = "Test"; + + ParseRelation relation = new ParseRelation(targetClass); + + assertNull(relation.getKey()); + assertNull(relation.getParent()); + assertEquals(targetClass, relation.getTargetClass()); + } + + @Test + public void testConstructorWithJSONAndDecoder() throws Exception { + // Make ParseRelation JSONArray + ParseObject object = mock(ParseObject.class); + when(object.getClassName()).thenReturn("Test"); + when(object.getObjectId()).thenReturn("objectId"); + object.setObjectId("objectId"); + JSONArray objectJSONArray = new JSONArray(); + objectJSONArray.put(PointerEncoder.get().encode(object)); + JSONObject relationJSON = new JSONObject(); + relationJSON.put("className", "Test"); + relationJSON.put("objects", objectJSONArray); + + ParseRelation relationFromJSON = new ParseRelation(relationJSON, ParseDecoder.get()); + + assertEquals("Test", relationFromJSON.getTargetClass()); + assertEquals(1, relationFromJSON.getKnownObjects().size()); + Object[] objects = relationFromJSON.getKnownObjects().toArray(); + assertEquals("objectId", ((ParseObject) objects[0]).getObjectId()); + } + + //endregion + + //region testParcelable + + @Test + public void testParcelable() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); + ParseRelation relation = new ParseRelation<>("Test"); + ParseObject parent = new ParseObject("Parent"); + parent.setObjectId("parentId"); + relation.ensureParentAndKey(parent, "key"); + ParseObject inner = new ParseObject("Test"); + inner.setObjectId("innerId"); + relation.add(inner); + + Parcel parcel = Parcel.obtain(); + relation.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + //noinspection unchecked + ParseRelation newRelation = ParseRelation.CREATOR.createFromParcel(parcel); + assertEquals(newRelation.getTargetClass(), "Test"); + assertEquals(newRelation.getKey(), "key"); + assertEquals(newRelation.getParent().getClassName(), "Parent"); + assertEquals(newRelation.getParent().getObjectId(), "parentId"); + assertEquals(newRelation.getKnownObjects().size(), 1); + + // This would fail assertTrue(newRelation.hasKnownObject(inner)). + // That is because ParseRelation uses == to check for known objects. + } + + //endregion + + //region testEnsureParentAndKey + + @Test + public void testEnsureParentAndKey() throws Exception { + ParseRelation relation = new ParseRelation("Test"); + + ParseObject parent = mock(ParseObject.class); + relation.ensureParentAndKey(parent, "key"); + + assertEquals(parent, relation.getParent()); + assertEquals("key", relation.getKey()); + } + + @Test + public void testEnsureParentAndKeyWithDifferentParent() throws Exception { + ParseRelation relation = new ParseRelation(mock(ParseObject.class), "key"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage( + "Internal error. One ParseRelation retrieved from two different ParseObjects."); + + relation.ensureParentAndKey(new ParseObject("Parent"), "key"); + } + + @Test + public void testEnsureParentAndKeyWithDifferentKey() throws Exception { + ParseObject parent = mock(ParseObject.class); + ParseRelation relation = new ParseRelation(parent, "key"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage( + "Internal error. One ParseRelation retrieved from two different keys."); + + relation.ensureParentAndKey(parent, "keyAgain"); + } + + //endregion + + //region testAdd + + @Test + public void testAdd() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseRelation relation = new ParseRelation(parent, "key"); + + ParseObject object = new ParseObject("Test"); + relation.add(object); + + // Make sure targetClass is updated + assertEquals("Test", relation.getTargetClass()); + // Make sure object is added to knownObjects + assertTrue(relation.hasKnownObject(object)); + // Make sure parent is updated + ParseRelation relationInParent = parent.getRelation("key"); + assertEquals("Test", relationInParent.getTargetClass()); + assertTrue(relationInParent.hasKnownObject(object)); + + } + + //endregion + + //region testRemove + + @Test + public void testRemove() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseRelation relation = new ParseRelation(parent, "key"); + + ParseObject object = new ParseObject("Test"); + relation.add(object); + + relation.remove(object); + + // Make sure targetClass does not change + assertEquals("Test", relation.getTargetClass()); + // Make sure object is removed from knownObjects + assertFalse(relation.hasKnownObject(object)); + // Make sure parent is updated + ParseRelation relationInParent = parent.getRelation("key"); + assertEquals("Test", relationInParent.getTargetClass()); + assertFalse(relation.hasKnownObject(object)); + } + + //endregion + + //region testGetQuery + + @Test + public void testGetQueryWithNoTargetClass() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseRelation relation = new ParseRelation(parent, "key"); + + ParseQuery query = relation.getQuery(); + + // Make sure className is correct + assertEquals("Parent", query.getClassName()); + ParseQuery.State state = query.getBuilder().build(); + // Make sure redirectClassNameForKey is set + assertEquals("key", state.extraOptions().get("redirectClassNameForKey")); + // Make sure where condition is set + ParseQuery.RelationConstraint relationConstraint = + (ParseQuery.RelationConstraint) state.constraints().get("$relatedTo"); + assertEquals("key", relationConstraint.getKey()); + assertSame(parent, relationConstraint.getObject()); + } + + @Test + public void testGetQueryWithTargetClass() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseRelation relation = new ParseRelation(parent, "key"); + relation.setTargetClass("targetClass"); + + ParseQuery query = relation.getQuery(); + + // Make sure className is correct + assertEquals("targetClass", query.getClassName()); + ParseQuery.State state = query.getBuilder().build(); + // Make sure where condition is set + ParseQuery.RelationConstraint relationConstraint = + (ParseQuery.RelationConstraint) state.constraints().get("$relatedTo"); + assertEquals("key", relationConstraint.getKey()); + assertSame(parent, relationConstraint.getObject()); + } + + //endregion + + //region testToJSON + + @Test + public void testEncodeToJSON() throws Exception { + ParseObject parent = new ParseObject("Parent"); + ParseRelation relation = new ParseRelation(parent, "key"); + relation.setTargetClass("Test"); + + ParseObject object = new ParseObject("Test"); + object.setObjectId("objectId"); + relation.addKnownObject(object); + + JSONObject json = relation.encodeToJSON(PointerEncoder.get()); + + assertEquals("Relation", json.getString("__type")); + assertEquals("Test", json.getString("className")); + JSONArray knownObjectsArray = json.getJSONArray("objects"); + assertEquals( + (JSONObject) PointerEncoder.get().encode(object), + knownObjectsArray.getJSONObject(0), + JSONCompareMode.NON_EXTENSIBLE); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRequestTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRequestTest.java new file mode 100644 index 0000000..759cff6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRequestTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpBody; +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ParseRequestTest { + + private static byte[] data; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void setUpClass() { + char[] chars = new char[64 << 10]; // 64KB + data = new String(chars).getBytes(); + } + + @AfterClass + public static void tearDownClass() { + data = null; + } + + @Before + public void setUp() { + ParseRequest.setDefaultInitialRetryDelay(1L); + } + + @After + public void tearDown() { + ParseRequest.setDefaultInitialRetryDelay(ParseRequest.DEFAULT_INITIAL_RETRY_DELAY); + } + + @Test + public void testRetryLogic() throws Exception { + ParseHttpClient mockHttpClient = mock(ParseHttpClient.class); + when(mockHttpClient.execute(any(ParseHttpRequest.class))).thenThrow(new IOException()); + + TestParseRequest request = new TestParseRequest(ParseHttpRequest.Method.GET, "http://parse.com"); + Task task = request.executeAsync(mockHttpClient); + task.waitForCompletion(); + + verify(mockHttpClient, times(5)).execute(any(ParseHttpRequest.class)); + } + + // TODO(grantland): Move to ParseFileRequestTest or ParseCountingByteArrayHttpBodyTest + @Test + public void testDownloadProgress() throws Exception { + ParseHttpResponse mockResponse = new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); + + ParseHttpClient mockHttpClient = mock(ParseHttpClient.class); + when(mockHttpClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + + File tempFile = temporaryFolder.newFile("test"); + ParseFileRequest request = + new ParseFileRequest(ParseHttpRequest.Method.GET, "localhost", tempFile); + TestProgressCallback downloadProgressCallback = new TestProgressCallback(); + Task task = request.executeAsync(mockHttpClient, null, downloadProgressCallback); + + task.waitForCompletion(); + assertFalse("Download failed: " + task.getError(), task.isFaulted()); + assertEquals(data.length, ParseFileUtils.readFileToByteArray(tempFile).length); + + assertProgressCompletedSuccessfully(downloadProgressCallback); + } + + private static void assertProgressCompletedSuccessfully(TestProgressCallback callback) { + int lastPercentDone = 0; + boolean incrementalPercentage = false; + for (int percentDone : callback.history) { + assertTrue("Progress went backwards", percentDone >= lastPercentDone); + assertTrue("Invalid percentDone: " + percentDone, percentDone >= 0 && percentDone <= 100); + + if (percentDone > 0 || percentDone < 100) { + incrementalPercentage = true; + } + + lastPercentDone = percentDone; + } + assertTrue("ProgressCallback was not called with a value between 0 and 100: " + callback.history, + incrementalPercentage); + assertEquals(100, callback.history.get(callback.history.size() - 1).intValue()); + } + + private static class TestProgressCallback implements ProgressCallback { + List history = new LinkedList<>(); + + @Override + public void done(Integer percentDone) { + history.add(percentDone); + } + } + + private static class TestParseRequest extends ParseRequest { + + public TestParseRequest(ParseHttpRequest.Method method, String url) { + super(method, url); + } + + byte[] data; + + @Override + protected Task onResponseAsync( + ParseHttpResponse response, ProgressCallback downloadProgressCallback) { + return Task.forResult(null); + } + + @Override + protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) { + if (uploadProgressCallback != null) { + return new ParseCountingByteArrayHttpBody(data, null, uploadProgressCallback); + } + return super.newBody(null); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRoleTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRoleTest.java new file mode 100644 index 0000000..9440c6e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseRoleTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +public class ParseRoleTest { + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseRole.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseRole.class); + } + + //region testConstructor + + @Test + public void testConstructorWithName() { + ParseRole role = new ParseRole("Test"); + + assertEquals("Test", role.getName()); + } + + @Test + public void testConstructorWithNameAndACL() { + ParseACL acl = new ParseACL(); + + ParseRole role = new ParseRole("Test", acl); + + assertEquals("Test", role.getName()); + assertSame(acl, role.getACL()); + } + + //endregion + + //region testSetName + + @Test + public void testSetName() { + ParseRole role = new ParseRole(); + + role.setName("Test"); + + assertEquals("Test", role.getName()); + } + + //endregion + + //region testGetUsers + + @Test + public void testGetUsers() { + ParseRole role = new ParseRole("Test"); + + assertThat(role.getUsers(), instanceOf(ParseRelation.class)); + assertSame(role.getUsers(), role.getRelation("users")); + } + + //endregion + + //region testGetRoles + + @Test + public void testGetRoles() { + ParseRole role = new ParseRole("Test"); + + assertThat(role.getRoles(), instanceOf(ParseRelation.class)); + assertSame(role.getRoles(), role.getRelation("roles")); + } + + //endregion + + //region testValidateSave + + @Test + public void testValidateSaveSuccess() { + ParseRole role = new ParseRole("Test"); + + role.validateSave(); + } + + @Test + public void testValidateSaveSuccessWithNoName() { + ParseRole role = new ParseRole("Test"); + role.setObjectId("test"); + + // objectId != null and name == null should not fail + role.validateSave(); + } + + @Test(expected = IllegalStateException.class) + public void testValidateSaveFailureWithNoObjectIdAndName() { + ParseRole role = new ParseRole(); + + role.validateSave(); + } + + //endregion + + //region testPut + + @Test + public void testPutSuccess() { + ParseRole role = new ParseRole("Test"); + + role.put("key", "value"); + + assertEquals("value", role.get("key")); + } + + @Test(expected = IllegalArgumentException.class) + public void testPutFailureWithNameAndObjectIdSet() { + ParseRole role = new ParseRole("Test"); + role.setObjectId("objectId"); + + role.put("name", "value"); + } + + @Test(expected = IllegalArgumentException.class) + public void testPutFailureWithInvalidNameTypeSet() { + ParseRole role = new ParseRole("Test"); + + role.put("name", 1); + } + + @Test(expected = IllegalArgumentException.class) + public void testPutFailureWithInvalidNameValueSet() { + ParseRole role = new ParseRole("Test"); + + role.put("name", "!!!!"); + } + + //endregion + + //region testGetQuery + + @Test + public void testGetQuery() { + ParseQuery query = ParseRole.getQuery(); + + assertEquals(ParseCorePlugins.getInstance().getSubclassingController().getClassName(ParseRole.class), query.getBuilder().getClassName()); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseSessionTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseSessionTest.java new file mode 100644 index 0000000..576a2ab --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseSessionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertTrue; + +public class ParseSessionTest { + + @Before + public void setUp() { + ParseObject.registerSubclass(ParseSession.class); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseSession.class); + } + + @Test + public void testImmutableKeys() { + String[] immutableKeys = { + "sessionToken", + "createdWith", + "restricted", + "user", + "expiresAt", + "installationId" + }; + + ParseSession session = new ParseSession(); + session.put("foo", "bar"); + session.put("USER", "bar"); + session.put("_user", "bar"); + session.put("token", "bar"); + + for (String immutableKey : immutableKeys) { + try { + session.put(immutableKey, "blah"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + session.remove(immutableKey); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + session.removeAll(immutableKey, Arrays.asList()); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTaskUtilsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTaskUtilsTest.java new file mode 100644 index 0000000..c44f940 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTaskUtilsTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +import bolts.AggregateException; +import bolts.Task; + +import static org.junit.Assert.assertTrue; + +public class ParseTaskUtilsTest { + /** + * Verifies {@link bolts.AggregateException} gets wrapped with {@link ParseException} when thrown from + * {@link com.parse.ParseTaskUtils#wait(bolts.Task)}. + */ + @Test + public void testWaitForTaskWrapsAggregateExceptionAsParseException() { + final Exception error = new RuntimeException("This task failed."); + + final ArrayList> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + final int number = i; + Task task = Task.callInBackground(new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep((long) (Math.random() * 100)); + if (number == 10 || number == 11) { + throw error; + } + return null; + } + }); + tasks.add(task); + } + + try { + ParseTaskUtils.wait(Task.whenAll(tasks)); + } catch (ParseException e) { + assertTrue(e.getCause() instanceof AggregateException); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTestUtils.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTestUtils.java new file mode 100644 index 0000000..13b29a3 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTestUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import com.parse.http.ParseHttpRequest; +import com.parse.http.ParseHttpResponse; + +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import bolts.Task; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** package */ final class ParseTestUtils { + + /** + * In our unit test, lots of controllers need to use currentParseUser. A normal + * currentUserController will try to read user off disk which will throw exception since we do + * not have android environment. This function register a mock currentUserController and simply + * return a mock ParseUser for currentParseUser and null for currentSessionToken. It will make + * ParseUser.getCurrentUserAsync() and ParseUser.getCurrentSessionTokenAsync() work. + */ + public static void setTestParseUser() { + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync()).thenReturn(Task.forResult(mock(ParseUser.class))); + when(currentUserController.getCurrentSessionTokenAsync()) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + } + + public static ParseHttpClient mockParseHttpClientWithResponse( + JSONObject content, int statusCode, String reasonPhrase) throws IOException { + byte[] contentBytes = content.toString().getBytes(); + ParseHttpResponse response = new ParseHttpResponse.Builder() + .setContent(new ByteArrayInputStream(contentBytes)) + .setStatusCode(statusCode) + .setTotalSize(contentBytes.length) + .setContentType("application/json") + .build(); + ParseHttpClient client = mock(ParseHttpClient.class); + when(client.execute(any(ParseHttpRequest.class))).thenReturn(response); + return client; + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTextUtilsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTextUtilsTest.java new file mode 100644 index 0000000..8f12d35 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseTextUtilsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ParseTextUtilsTest { + + //region testJoin + + @Test + public void testJoinMultipleItems() { + String joined = ParseTextUtils.join(",", Arrays.asList("one", "two", "three")); + assertEquals("one,two,three", joined); + } + + @Test + public void testJoinSingleItem() { + String joined = ParseTextUtils.join(",", Collections.singletonList("one")); + assertEquals("one", joined); + } + + //endregion + + //region testIsEmpty + + @Test + public void testEmptyStringIsEmpty() { + assertTrue(ParseTextUtils.isEmpty("")); + } + + @Test + public void testNullStringIsEmpty() { + assertTrue(ParseTextUtils.isEmpty(null)); + } + + @Test + public void testStringIsNotEmpty() { + assertFalse(ParseTextUtils.isEmpty("not empty")); + } + + //endregion + + //region testEquals + + @Test + public void testEqualsNull() { + assertTrue(ParseTextUtils.equals(null, null)); + } + + @Test + public void testNotEqualsNull() { + assertFalse(ParseTextUtils.equals("not null", null)); + assertFalse(ParseTextUtils.equals(null, "not null")); + } + + @Test + public void testEqualsString() { + String same = "Hello, world!"; + assertTrue(ParseTextUtils.equals(same, same)); + assertTrue(ParseTextUtils.equals(same, same + "")); // Hack to compare different instances + } + + @Test + public void testNotEqualsString() { + assertFalse(ParseTextUtils.equals("grantland", "nlutsenko")); + } + + //endregion +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserCurrentCoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserCurrentCoderTest.java new file mode 100644 index 0000000..aef1fec --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserCurrentCoderTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +public class ParseUserCurrentCoderTest { + + private static final String KEY_AUTH_DATA = "auth_data"; + private static final String KEY_SESSION_TOKEN = "session_token"; + + @Test + public void testEncodeSuccess() throws Exception { + Map facebookAuthData = new HashMap<>(); + facebookAuthData.put("id", "facebookId"); + facebookAuthData.put("access_token", "facebookAccessToken"); + Map twitterAuthData = new HashMap<>(); + twitterAuthData.put("id", "twitterId"); + twitterAuthData.put("access_token", "twitterAccessToken"); + ParseUser.State state = new ParseUser.State.Builder() + .sessionToken("sessionToken") + .putAuthData("facebook", facebookAuthData) + .putAuthData("twitter", twitterAuthData) + .build(); + + ParseUserCurrentCoder coder = ParseUserCurrentCoder.get(); + JSONObject objectJson = coder.encode(state, null, PointerEncoder.get()); + + assertEquals("sessionToken", objectJson.getString(KEY_SESSION_TOKEN)); + JSONObject authDataJson = objectJson.getJSONObject(KEY_AUTH_DATA); + JSONObject facebookAuthDataJson = authDataJson.getJSONObject("facebook"); + assertEquals("facebookId", facebookAuthDataJson.getString("id")); + assertEquals("facebookAccessToken", facebookAuthDataJson.getString("access_token")); + JSONObject twitterAuthDataJson = authDataJson.getJSONObject("twitter"); + assertEquals("twitterId", twitterAuthDataJson.getString("id")); + assertEquals("twitterAccessToken", twitterAuthDataJson.getString("access_token")); + } + + @Test + public void testEncodeSuccessWithEmptyState() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .build(); + + ParseUserCurrentCoder coder = ParseUserCurrentCoder.get(); + JSONObject objectJson = coder.encode(state, null, PointerEncoder.get()); + + assertFalse(objectJson.has(KEY_SESSION_TOKEN)); + assertFalse(objectJson.has(KEY_AUTH_DATA)); + } + + @Test + public void testDecodeSuccessWithSessionTokenAndAuthData() throws Exception { + JSONObject facebookAuthDataJson = new JSONObject() + .put("id", "facebookId") + .put("access_token", "facebookAccessToken"); + JSONObject twitterAuthDataJson = new JSONObject() + .put("id", "twitterId") + .put("access_token", "twitterAccessToken"); + JSONObject authDataJson = new JSONObject() + .put("facebook", facebookAuthDataJson) + .put("twitter", twitterAuthDataJson); + JSONObject objectJson = new JSONObject() + .put(KEY_SESSION_TOKEN, "sessionToken") + .put(KEY_AUTH_DATA, authDataJson); + + ParseUserCurrentCoder coder = ParseUserCurrentCoder.get(); + ParseUser.State.Builder builder = + coder.decode(new ParseUser.State.Builder(), objectJson, ParseDecoder.get()); + + // We use the builder to build a state to verify the content in the builder + ParseUser.State state = builder.build(); + assertEquals("sessionToken", state.sessionToken()); + Map> authData = state.authData(); + Map facebookAuthData = authData.get("facebook"); + assertEquals("facebookId", facebookAuthData.get("id")); + assertEquals("facebookAccessToken", facebookAuthData.get("access_token")); + Map twitterAuthData = authData.get("twitter"); + assertEquals("twitterId", twitterAuthData.get("id")); + assertEquals("twitterAccessToken", twitterAuthData.get("access_token")); + } + + @Test + public void testDecodeSuccessWithoutSessionTokenAndAuthData() throws Exception { + JSONObject objectJson = new JSONObject(); + + ParseUserCurrentCoder coder = ParseUserCurrentCoder.get(); + ParseUser.State.Builder builder = + coder.decode(new ParseUser.State.Builder(), objectJson, ParseDecoder.get()); + + // We use the builder to build a state to verify the content in the builder + ParseUser.State state = builder.build(); + assertNull(state.sessionToken()); + // We always return non-null for authData() + assertEquals(0, state.authData().size()); + } + + @Test + public void testEncodeDecodeWithNullValues() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .sessionToken(null) + .authData(null) + .build(); + ParseUserCurrentCoder coder = ParseUserCurrentCoder.get(); + JSONObject object = coder.encode(state, null, PointerEncoder.get()); + ParseUser.State.Builder builder = + coder.decode(new ParseUser.State.Builder(), object, ParseDecoder.get()); + state = builder.build(); + assertNull(state.sessionToken()); + assertEquals(0, state.authData().size()); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserTest.java new file mode 100644 index 0000000..b5fe54f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ParseUserTest.java @@ -0,0 +1,1544 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import bolts.Task; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +// For ParseExecutors.main() +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class ParseUserTest extends ResetPluginsParseTest { + + @Rule + public ExpectedException thrown= ExpectedException.none(); + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseObject.registerSubclass(ParseUser.class); + ParseObject.registerSubclass(ParseSession.class); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + ParseObject.unregisterSubclass(ParseUser.class); + ParseObject.unregisterSubclass(ParseSession.class); + Parse.disableLocalDatastore(); + } + + @Test + public void testImmutableKeys() { + ParseUser user = new ParseUser(); + user.put("foo", "bar"); + + try { + user.put("sessionToken", "blah"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + user.remove("sessionToken"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + + try { + user.removeAll("sessionToken", Collections.emptyList()); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot modify")); + } + } + + // region Parcelable + + @Test + public void testOnSaveRestoreState() throws Exception { + ParseUser user = new ParseUser(); + user.setObjectId("objId"); + user.setIsCurrentUser(true); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isCurrentUser()); + } + + @Test + public void testParcelableState() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .objectId("test") + .isNew(true) + .build(); + ParseUser user = ParseObject.from(state); + assertTrue(user.isNew()); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isNew()); + } + + // endregion + + //region SignUpAsync + + @Test + public void testSignUpAsyncWithNoUserName() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Username cannot be missing or blank"); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + } + + @Test + public void testSignUpAsyncWithNoPassword() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setUsername("userName"); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Password cannot be missing or blank"); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + } + + @Test + public void testSignUpAsyncWithObjectIdSetAndAuthDataNotSet() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .build(); + ParseUser user = ParseObject.from(userState); + user.setUsername("userName"); + user.setPassword("password"); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Cannot sign up a user that has already signed up."); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + } + + @Test + public void testSignUpAsyncWithObjectIdSetAndAuthDataSet() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = mock(ParseUser.class); + when(currentUser.getSessionToken()).thenReturn("sessionToken"); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, null) + .build(); + ParseUser user = ParseObject.from(userState); + user.setUsername("userName"); + user.setPassword("password"); + //TODO (mengyan): Avoid using partial mock after we have ParseObjectInstanceController + ParseUser partialMockUser = spy(user); + doReturn(Task.forResult(null)) + .when(partialMockUser) + .saveAsync(anyString(), Matchers.>any()); + + ParseTaskUtils.wait(partialMockUser.signUpAsync(Task.forResult(null))); + + // Verify user is saved + verify(partialMockUser, times(1)).saveAsync(eq("sessionToken"), Matchers.>any()); + } + + @Test + public void testSignUpAsyncWithAnotherSignUpAlreadyRunning() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + user.startSave(); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Cannot sign up a user that is already signing up."); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + } + + @Test + public void testSignUpAsyncWithSignUpSameAnonymousUser() throws Exception { + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("key", "token"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(user)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attempt to merge currentUser with itself."); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + } + + @Test + public void testSignUpAsyncWithMergeInDiskAnonymousUser() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = mock(ParseUser.class); + when(currentUser.getUsername()).thenReturn("oldUserName"); + when(currentUser.getPassword()).thenReturn("oldPassword"); + when(currentUser.isLazy()).thenReturn(false); + when(currentUser.isLinked(ParseAnonymousUtils.AUTH_TYPE)).thenReturn(true); + when(currentUser.getSessionToken()).thenReturn("oldSessionToken"); + when(currentUser.getAuthData()).thenReturn(new HashMap>()); + when(currentUser.saveAsync(anyString(), eq(false), Matchers.>any())) + .thenReturn(Task.forResult(null)); + ParseUser.State state = new ParseUser.State.Builder() + .put("oldKey", "oldValue") + .build(); + when(currentUser.getState()).thenReturn(state); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("key", "token"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + + Task signUpTask = user.signUpAsync(Task.forResult(null)); + signUpTask.waitForCompletion(); + + // Make sure currentUser copy changes from user + verify(currentUser, times(1)).copyChangesFrom(user); + // Make sure we update currentUser username and password + verify(currentUser, times(1)).setUsername("userName"); + verify(currentUser, times(1)).setPassword("password"); + // Make sure we save currentUser + verify(currentUser, times(1)) + .saveAsync(eq("oldSessionToken"), eq(false), Matchers.>any()); + // Make sure we merge currentUser with user after save + assertEquals("oldValue", user.get("oldKey")); + // Make sure set currentUser + verify(currentUserController, times(1)).setAsync(eq(user)); + } + + @Test + public void testSignUpAsyncWithMergeInDiskAnonymousUserSaveFailure() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + Map oldAnonymousAuthData = new HashMap<>(); + oldAnonymousAuthData.put("oldKey", "oldToken"); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData); + ParseUser partialMockCurrentUser = spy(currentUser); // Spy since we need mutex + when(partialMockCurrentUser.getUsername()).thenReturn("oldUserName"); + when(partialMockCurrentUser.getPassword()).thenReturn("oldPassword"); + when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken"); + when(partialMockCurrentUser.isLazy()).thenReturn(false); + ParseException saveException = new ParseException(ParseException.OTHER_CAUSE, ""); + doReturn(Task.forError(saveException)) + .when(partialMockCurrentUser) + .saveAsync(anyString(), eq(false), Matchers.>any()); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())) + .thenReturn(Task.forResult(partialMockCurrentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("key", "token"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + + Task signUpTask = user.signUpAsync(Task.forResult(null)); + signUpTask.waitForCompletion(); + + // Make sure we update currentUser username and password + verify(partialMockCurrentUser, times(1)).setUsername("userName"); + verify(partialMockCurrentUser, times(1)).setPassword("password"); + // Make sure we sync user with currentUser + verify(partialMockCurrentUser, times(1)).copyChangesFrom(eq(user)); + // Make sure we save currentUser + verify(partialMockCurrentUser, times(1)) + .saveAsync(eq("oldSessionToken"), eq(false), Matchers.>any()); + // Make sure we restore old username and password after save fails + verify(partialMockCurrentUser, times(1)).setUsername("oldUserName"); + verify(partialMockCurrentUser, times(1)).setPassword("oldPassword"); + // Make sure we restore anonymity + verify(partialMockCurrentUser, times(1)).putAuthData( + ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData); + // Make sure task is failed + assertTrue(signUpTask.isFaulted()); + assertSame(saveException, signUpTask.getError()); + } + + @Test + public void testSignUpAsyncWithNoCurrentUserAndSignUpSuccess() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .build(); + when(userController.signUpAsync( + any(ParseUser.State.class), any(ParseOperationSet.class), anyString())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + + ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null))); + + // Make sure we sign up the user + verify(userController, times(1)).signUpAsync( + any(ParseUser.State.class), any(ParseOperationSet.class), anyString()); + // Make sure user's data is correct + assertEquals("newSessionToken", user.getSessionToken()); + assertEquals("newValue", user.getString("newKey")); + assertFalse(user.isLazy()); + // Make sure we set the current user + verify(currentUserController, times(1)).setAsync(user); + } + + @Test + public void testSignUpAsyncWithNoCurrentUserAndSignUpFailure() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseException signUpException = new ParseException(ParseException.OTHER_CAUSE, "test"); + when(userController.signUpAsync( + any(ParseUser.State.class), any(ParseOperationSet.class), anyString())) + .thenReturn(Task.forError(signUpException)); + ParseCorePlugins.getInstance().registerUserController(userController); + + ParseUser user = new ParseUser(); + user.put("key", "value"); + user.setUsername("userName"); + user.setPassword("password"); + + Task signUpTask = user.signUpAsync(Task.forResult(null)); + + // Make sure we sign up the user + verify(userController, times(1)).signUpAsync( + any(ParseUser.State.class), any(ParseOperationSet.class), anyString()); + // Make sure user's data is correct + assertEquals("value", user.getString("key")); + // Make sure we never set the current user + verify(currentUserController, never()).setAsync(user); + // Make sure task is failed + assertTrue(signUpTask.isFaulted()); + assertSame(signUpException, signUpTask.getError()); + } + + //endregion + + //region testLogInWithAsync + + @Test + public void testLoginWithAsyncWithoutExistingLazyUser() throws ParseException { + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(false)).thenReturn(Task.forResult(null)); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + + ParseUser.State userState = mock(ParseUser.State.class); + when(userState.className()).thenReturn("_User"); + when(userState.objectId()).thenReturn("1234"); + when(userState.isComplete()).thenReturn(true); + + ParseUserController userController = mock(ParseUserController.class); + when(userController.logInAsync(anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Task.forResult(userState)); + + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + ParseCorePlugins.getInstance().registerUserController(userController); + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + ParseUser user = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData)); + + verify(currentUserController).getAsync(false); + verify(userController).logInAsync(authType, authData); + verify(currentUserController).setAsync(user); + assertSame(userState, user.getState()); + + verifyNoMoreInteractions(currentUserController); + verifyNoMoreInteractions(userController); + } + + @Test + public void testLoginWithAsyncWithLinkedLazyUser() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + setLazy(currentUser); + ParseUser partialMockCurrentUser = spy(currentUser); + when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken"); + doReturn(Task.forResult(null)) + .when(partialMockCurrentUser) + .resolveLazinessAsync(Matchers.>any()); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(false)).thenReturn(Task.forResult(partialMockCurrentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + ParseUser userAfterLogin = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, + authData)); + + // Make sure we stripAnonymity + assertNull(userAfterLogin.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE)); + // Make sure we update authData + assertEquals(authData, userAfterLogin.getAuthData().get("facebook")); + // Make sure we resolveLaziness + verify(partialMockCurrentUser, times(1)).resolveLazinessAsync(Matchers.>any()); + } + + @Test + public void testLoginWithAsyncWithLinkedLazyUseAndResolveLazinessFailure() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + Map oldAnonymousAuthData = new HashMap<>(); + oldAnonymousAuthData.put("oldKey", "oldToken"); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData); + ParseUser partialMockCurrentUser = spy(currentUser); + when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken"); + doReturn(Task.forError(new Exception())) + .when(partialMockCurrentUser) + .resolveLazinessAsync(Matchers.>any()); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(false)).thenReturn(Task.forResult(partialMockCurrentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + + Task loginTask = ParseUser.logInWithInBackground(authType, authData); + loginTask.waitForCompletion(); + + // Make sure we try to resolveLaziness + verify(partialMockCurrentUser, times(1)).resolveLazinessAsync(Matchers.>any()); + // Make sure we do not save new authData + assertNull(partialMockCurrentUser.getAuthData().get("facebook")); + // Make sure we restore anonymity after resolve laziness failure + assertEquals(oldAnonymousAuthData, partialMockCurrentUser.getAuthData() + .get(ParseAnonymousUtils.AUTH_TYPE)); + // Make sure task fails + assertTrue(loginTask.isFaulted()); + } + + @Test + public void testLoginWithAsyncWithLinkedNotLazyUser() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser.State state = new ParseUser.State.Builder() + .objectId("objectId") // Make it not lazy + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .build(); + ParseUser currentUser = ParseUser.from(state); + ParseUser partialMockCurrentUser = spy(currentUser); // ParseUser.mutex + doReturn(Task.forResult(null)) + .when(partialMockCurrentUser) + .linkWithInBackground(anyString(), Matchers.>any()); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync()).thenReturn(Task.forResult(partialMockCurrentUser)); + when(currentUserController.getAsync(anyBoolean())) + .thenReturn(Task.forResult(partialMockCurrentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + + ParseUser userAfterLogin = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, + authData)); + + // Make sure we link authData + verify(partialMockCurrentUser, times(1)).linkWithInBackground(authType, authData); + assertSame(partialMockCurrentUser, userAfterLogin); + } + + @Test + public void testLoginWithAsyncWithLinkedNotLazyUserLinkFailure() throws Exception { + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .build(); + when(userController.logInAsync(anyString(), Matchers.>any())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + currentUser.setObjectId("objectId"); // Make it not lazy. + ParseUser partialMockCurrentUser = spy(currentUser); + when(partialMockCurrentUser.getSessionToken()).thenReturn("sessionToken"); + ParseException linkException = + new ParseException(ParseException.ACCOUNT_ALREADY_LINKED, "Account already linked"); + doReturn(Task.forError(linkException)) + .when(partialMockCurrentUser) + .linkWithInBackground(anyString(), Matchers.>any()); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(false)).thenReturn(Task.forResult(partialMockCurrentUser)); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + ParseUser userAfterLogin = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, + authData)); + + // Make sure we link authData + verify(partialMockCurrentUser, times(1)).linkWithInBackground(authType, authData); + // Make sure we login authData + verify(userController, times(1)).logInAsync("facebook", authData); + // Make sure we save the new created user as currentUser + verify(currentUserController, times(1)).setAsync(any(ParseUser.class)); + // Make sure the new created user has correct data + assertEquals("newValue", userAfterLogin.get("newKey")); + assertEquals("newSessionToken", userAfterLogin.getSessionToken()); + } + + @Test + public void testLoginWithAsyncWithNoCurrentUser() throws Exception { + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .build(); + when(userController.logInAsync(anyString(), Matchers.>any())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(false)).thenReturn(Task.forResult(null)); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("token", "123"); + + ParseUser userAfterLogin = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, + authData)); + + // Make sure we login authData + verify(userController, times(1)).logInAsync("facebook", authData); + // Make sure we save the new created user as currentUser + verify(currentUserController, times(1)).setAsync(any(ParseUser.class)); + // Make sure the new created user has correct data + assertEquals("newValue", userAfterLogin.get("newKey")); + assertEquals("newSessionToken", userAfterLogin.getSessionToken()); + } + + //endregion + + //region testlinkWithInBackground + + @Test + public void testlinkWithInBackgroundWithSaveAsyncSuccess() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getCurrentSessionTokenAsync()) + .thenReturn(Task.forResult(null)); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register mock callbacks + AuthenticationCallback callbacks = mock(AuthenticationCallback.class); + when(callbacks.onRestore(Matchers.>any())) + .thenReturn(true); + ParseUser.registerAuthenticationCallback("facebook", callbacks); + + ParseUser user = new ParseUser(); + // To make synchronizeAuthData work + user.setIsCurrentUser(true); + // To verify stripAnonymity + user.setObjectId("objectId"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + ParseUser partialMockUser = spy(user); + doReturn(Task.forResult(null)) + .when(partialMockUser) + .saveAsync(anyString(), eq(false), Matchers.>any()); + doReturn("sessionTokenAgain") + .when(partialMockUser) + .getSessionToken(); + Map authData = new HashMap<>(); + authData.put("token", "test"); + + ParseTaskUtils.wait(partialMockUser.linkWithInBackground("facebook", authData)); + + // Make sure we stripAnonymity + assertNull(partialMockUser.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE)); + // Make sure new authData is added + assertSame(authData, partialMockUser.getAuthData().get("facebook")); + // Make sure we save the user + verify(partialMockUser, times(1)) + .saveAsync(eq("sessionTokenAgain"), eq(false), Matchers.>any()); + // Make sure synchronizeAuthData() is called + verify(callbacks, times(1)).onRestore(authData); + } + + @Test + public void testlinkWithInBackgroundWithSaveAsyncFailure() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getCurrentSessionTokenAsync()) + .thenReturn(Task.forResult("sessionToken")); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("anonymousToken", "anonymousTest"); + // To verify stripAnonymity + user.setObjectId("objectId"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + ParseUser partialMockUser = spy(user); + Exception saveException = new Exception(); + doReturn(Task.forError(saveException)) + .when(partialMockUser) + .saveAsync(anyString(), eq(false), Matchers.>any()); + doReturn("sessionTokenAgain") + .when(partialMockUser) + .getSessionToken(); + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("facebookToken", "facebookTest"); + + Task linkTask = + partialMockUser.linkWithInBackground(authType, authData); + linkTask.waitForCompletion(); + + // Make sure we save the user + verify(partialMockUser, times(1)) + .saveAsync(eq("sessionTokenAgain"), eq(false), Matchers.>any()); + // Make sure old authData is restored + assertSame(anonymousAuthData, partialMockUser.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE)); + // Make sure failed new authData is cleared + assertNull(partialMockUser.getAuthData().get("facebook")); + // Verify exception + assertSame(saveException, linkTask.getError()); + } + + //endregion + + //region testResolveLazinessAsync + + @Test + public void testResolveLazinessAsyncWithAuthDataAndNotNewUser() throws Exception { + ParseUser user = new ParseUser(); + setLazy(user); + user.putAuthData("facebook", new HashMap()); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .isNew(false) + .build(); + when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class))) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null))); + ArgumentCaptor userAfterResolveLazinessCaptor = + ArgumentCaptor.forClass(ParseUser.class); + + // Make sure we logIn the lazy user + verify(userController, times(1)).logInAsync( + any(ParseUser.State.class), any(ParseOperationSet.class)); + // Make sure we save currentUser + verify(currentUserController, times(1)).setAsync(userAfterResolveLazinessCaptor.capture()); + ParseUser userAfterResolveLaziness = userAfterResolveLazinessCaptor.getValue(); + // Make sure user's data is correct + assertEquals("newSessionToken", userAfterResolveLaziness.getSessionToken()); + assertEquals("newValue", userAfterResolveLaziness.get("newKey")); + // Make sure userAfterResolveLaziness is not lazy + assertFalse(userAfterResolveLaziness.isLazy()); + // Make sure we create new user + assertNotSame(user, userAfterResolveLaziness); + } + + @Test + public void testResolveLazinessAsyncWithAuthDataAndNewUser() throws Exception { + ParseUser user = new ParseUser(); + setLazy(user); + user.putAuthData("facebook", new HashMap()); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .objectId("objectId") + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .isNew(true) + .build(); + when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class))) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + // Register a mock currentUserController to verify setAsync + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null))); + + // Make sure we logIn the lazy user + verify(userController, times(1)).logInAsync( + any(ParseUser.State.class), any(ParseOperationSet.class)); + // Make sure we do not save currentUser + verify(currentUserController, never()).setAsync(any(ParseUser.class)); + // Make sure userAfterResolveLaziness's data is correct + assertEquals("newSessionToken", user.getSessionToken()); + assertEquals("newValue", user.get("newKey")); + // Make sure userAfterResolveLaziness is not lazy + assertFalse(user.isLazy()); + } + + @Test + public void testResolveLazinessAsyncWithAuthDataAndNotNewUserAndLDSEnabled() throws Exception { + ParseUser user = new ParseUser(); + setLazy(user); + user.putAuthData("facebook", new HashMap()); + // To verify handleSaveResultAsync is not called + user.setPassword("password"); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .isNew(false) + .build(); + when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class))) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + // Register a mock currentUserController to make getCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Enable LDS + Parse.enableLocalDatastore(null); + + ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null))); + ArgumentCaptor userAfterResolveLazinessCaptor = + ArgumentCaptor.forClass(ParseUser.class); + + // Make sure we logIn the lazy user + verify(userController, times(1)).logInAsync( + any(ParseUser.State.class), any(ParseOperationSet.class)); + // Make sure handleSaveResultAsync() is not called, if handleSaveResultAsync is called, password + // field should be cleaned + assertEquals("password", user.getPassword()); + // Make sure we do not save currentUser + verify(currentUserController, times(1)).setAsync(userAfterResolveLazinessCaptor.capture()); + ParseUser userAfterResolveLaziness = userAfterResolveLazinessCaptor.getValue(); + // Make sure userAfterResolveLaziness's data is correct + assertEquals("newSessionToken", userAfterResolveLaziness.getSessionToken()); + assertEquals("newValue", userAfterResolveLaziness.get("newKey")); + // Make sure userAfterResolveLaziness is not lazy + assertFalse(userAfterResolveLaziness.isLazy()); + // Make sure we create new user + assertNotSame(user, userAfterResolveLaziness); + } + + //endregion + + //region testValidateSave + + @Test + public void testValidateSaveWithNoObjectId() throws Exception { + ParseUser user = new ParseUser(); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Cannot save a ParseUser until it has been signed up. Call signUp first."); + + user.validateSave(); + } + + // TODO(mengyan): Add testValidateSaveWithIsAuthenticatedWithNotDirty + + // TODO(mengyan): Add testValidateSaveWithIsAuthenticatedWithIsCurrentUser + + @Test + public void testValidateSaveWithLDSNotEnabled() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.setObjectId("test"); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setObjectId("test"); + // Make isDirty return true + user.put("key", "value"); + // Make isCurrent return false + user.setIsCurrentUser(false); + + user.validateSave(); + } + + @Test + public void testValidateSaveWithLDSNotEnabledAndCurrentUserNotMatch() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.setObjectId("testAgain"); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setObjectId("test"); + // Make isDirty return true + user.put("key", "value"); + // Make isCurrent return false + user.setIsCurrentUser(false); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Cannot save a ParseUser that is not authenticated."); + + user.validateSave(); + } + + //endregion + + //region testSaveAsync + + @Test + public void testSaveAsyncWithLazyAndCurrentUser() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + // Set facebook authData to null to verify cleanAuthData() + ParseUser.State userState = new ParseUser.State.Builder() + .putAuthData("facebook", null) + .build(); + ParseUser user = ParseObject.from(userState); + setLazy(user); + user.setIsCurrentUser(true); + ParseUser partialMockUser = spy(user); + doReturn(Task.forResult(null)) + .when(partialMockUser) + .resolveLazinessAsync(Matchers.>any()); + + ParseTaskUtils.wait(partialMockUser.saveAsync("sessionToken", Task.forResult(null))); + + // Make sure we clean authData + assertFalse(partialMockUser.getAuthData().containsKey("facebook")); + // Make sure we save new currentUser + verify(currentUserController, times(1)).setAsync(partialMockUser); + } + + @Test + public void testSaveAsyncWithLazyAndNotCurrentUser() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + // Set facebook authData to null to verify cleanAuthData() + ParseUser.State userState = new ParseUser.State.Builder() + .putAuthData("facebook", null) + .build(); + ParseUser user = ParseObject.from(userState); + setLazy(user); + user.setIsCurrentUser(false); + ParseUser partialMockUser = spy(user); + doReturn(Task.forResult(null)) + .when(partialMockUser) + .resolveLazinessAsync(Matchers.>any()); + + ParseTaskUtils.wait(partialMockUser.saveAsync("sessionToken", Task.forResult(null))); + + // Make sure we do not clean authData + assertTrue(partialMockUser.getAuthData().containsKey("facebook")); + // Make sure we do not save new currentUser + verify(currentUserController, never()).setAsync(partialMockUser); + } + + // TODO(mengyan): Add testSaveAsyncWithNotLazyAndNotCurrentUser, right now we can not mock + // super.save() + + //endregion + + //region testLogOutAsync + + @Test + public void testLogOutAsync() throws Exception { + // Register a mock sessionController to verify revokeAsync() + NetworkSessionController sessionController = mock(NetworkSessionController.class); + when(sessionController.revokeAsync(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerSessionController(sessionController); + ParseAuthenticationManager manager = mock(ParseAuthenticationManager.class); + when(manager.deauthenticateAsync(anyString())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerAuthenticationManager(manager); + + // Set user initial state + String facebookAuthType = "facebook"; + Map facebookAuthData = new HashMap<>(); + facebookAuthData.put("facebookToken", "facebookTest"); + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .putAuthData(facebookAuthType, facebookAuthData) + .sessionToken("r:oldSessionToken") + .build(); + ParseUser user = ParseObject.from(userState); + + ParseTaskUtils.wait(user.logOutAsync()); + + verify(manager).deauthenticateAsync("facebook"); + // Verify we revoke session + verify(sessionController, times(1)).revokeAsync("r:oldSessionToken"); + } + + //endregion + + //region testEnable/UpgradeSessionToken + + @Test + public void testEnableRevocableSessionInBackgroundWithCurrentUser() throws Exception { + // Register a mock ParsePlugins to make restClient() work + ParsePlugins mockPlugins = mock(ParsePlugins.class); + when(mockPlugins.restClient()).thenReturn(null); + ParsePlugins.set(mockPlugins); + // Register a mock currentUserController to verify setAsync + ParseUser mockUser = mock(ParseUser.class); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(mockUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseTaskUtils.wait(ParseUser.enableRevocableSessionInBackground()); + + verify(currentUserController, times(1)).getAsync(false); + verify(mockUser, times(1)).upgradeToRevocableSessionAsync(); + } + + @Test + public void testEnableRevocableSessionInBackgroundWithNoCurrentUser() throws Exception { + // Register a mock ParsePlugins to make restClient() work + ParsePlugins mockPlugins = mock(ParsePlugins.class); + when(mockPlugins.restClient()).thenReturn(null); + ParsePlugins.set(mockPlugins); + // Register a mock currentUserController to verify setAsync + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseTaskUtils.wait(ParseUser.enableRevocableSessionInBackground()); + + verify(currentUserController, times(1)).getAsync(false); + } + + @Test + public void testUpgradeToRevocableSessionAsync() throws Exception { + // Register a mock currentUserController to verify setAsync + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock sessionController to verify revokeAsync() + NetworkSessionController sessionController = mock(NetworkSessionController.class); + ParseSession.State state = new ParseSession.State.Builder("_Session") + .put("sessionToken", "r:newSessionToken") + .build(); + when(sessionController.upgradeToRevocable(anyString())) + .thenReturn(Task.forResult(state)); + ParseCorePlugins.getInstance().registerSessionController(sessionController); + + // Set user initial state + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .sessionToken("oldSessionToken") + .build(); + ParseUser user = ParseObject.from(userState); + + ParseTaskUtils.wait(user.upgradeToRevocableSessionAsync()); + + // Make sure we update to new sessionToken + assertEquals("r:newSessionToken", user.getSessionToken()); + // Make sure we update currentUser + verify(currentUserController, times(1)).setAsync(user); + } + + @Test + public void testDontOverwriteSessionTokenForCurrentUser() throws Exception { + ParseUser.State sessionTokenState = new ParseUser.State.Builder() + .sessionToken("sessionToken") + .put("key0", "value0") + .put("key1", "value1") + .isComplete(true) + .build(); + ParseUser.State newState = new ParseUser.State.Builder() + .put("key0", "newValue0") + .put("key2", "value2") + .isComplete(true) + .build(); + ParseUser.State emptyState = new ParseUser.State.Builder().isComplete(true).build(); + + ParseUser user = ParseObject.from(sessionTokenState); + user.setIsCurrentUser(true); + assertEquals(user.getSessionToken(), "sessionToken"); + assertEquals(user.getString("key0"), "value0"); + assertEquals(user.getString("key1"), "value1"); + + user.setState(newState); + assertEquals(user.getSessionToken(), "sessionToken"); + assertEquals(user.getString("key0"), "newValue0"); + assertNull(user.getString("key1")); + assertEquals(user.getString("key2"), "value2"); + + user.setIsCurrentUser(false); + user.setState(emptyState); + assertNull(user.getSessionToken()); + assertNull(user.getString("key0")); + assertNull(user.getString("key1")); + assertNull(user.getString("key2")); + } + + //endregion + + //region testUnlinkFromAsync + + @Test + public void testUnlinkFromAsyncWithAuthType() throws Exception { + // Register a mock currentUserController to make getAsync work + ParseUser mockUser = mock(ParseUser.class); + when(mockUser.getSessionToken()).thenReturn("sessionToken"); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync()).thenReturn(Task.forResult(mockUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + // Set user initial state + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("facebookToken", "facebookTest"); + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .putAuthData(authType, authData) + .build(); + ParseUser user = ParseObject.from(userState); + ParseUser partialMockUser = spy(user); + doReturn(Task.forResult(null)) + .when(partialMockUser) + .saveAsync(anyString(), Matchers.>any()); + + ParseTaskUtils.wait(partialMockUser.unlinkFromInBackground(authType)); + + // Verify we delete authData + assertNull(user.getAuthData().get("facebook")); + // Verify we save the user + verify(partialMockUser, times(1)).saveAsync(eq("sessionToken"), Matchers.>any()); + } + + //endregion + + //region testLogin + + @Test + public void testLogInInWithNoUserName() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Must specify a username for the user to log in with"); + + ParseTaskUtils.wait(ParseUser.logInInBackground(null, "password")); + } + + @Test + public void testLogInWithNoPassword() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Must specify a password for the user to log in with"); + + ParseTaskUtils.wait(ParseUser.logInInBackground("userName", null)); + } + + @Test + public void testLogIn() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .build(); + when(userController.logInAsync(anyString(), anyString())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + + ParseUser user = ParseUser.logIn("userName", "password"); + + // Make sure user is login + verify(userController, times(1)).logInAsync("userName", "password"); + // Make sure we set currentUser + verify(currentUserController, times(1)).setAsync(user); + // Make sure user's data is correct + assertEquals("newSessionToken", user.getSessionToken()); + assertEquals("newValue", user.get("newKey")); + } + + @Test + public void testLogInWithCallback() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make logIn work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("newKey", "newValue") + .sessionToken("newSessionToken") + .build(); + when(userController.logInAsync(anyString(), anyString())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + + final Semaphore done = new Semaphore(0); + ParseUser.logInInBackground("userName", "password", new LogInCallback() { + @Override + public void done(ParseUser user, ParseException e) { + done.release(); + assertNull(e); + // Make sure user's data is correct + assertEquals("newSessionToken", user.getSessionToken()); + assertEquals("newValue", user.get("newKey")); + } + }); + + assertTrue(done.tryAcquire(5, TimeUnit.SECONDS)); + // Make sure user is login + verify(userController, times(1)).logInAsync("userName", "password"); + // Make sure we set currentUser + verify(currentUserController, times(1)).setAsync(any(ParseUser.class)); + } + + //endregion + + //region testBecome + + @Test + public void testBecomeWithNoSessionToken() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Must specify a sessionToken for the user to log in with"); + + ParseUser.become(null); + } + + @Test + public void testBecome() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make getUsreAsync work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("key", "value") + .sessionToken("sessionToken") + .build(); + when(userController.getUserAsync(anyString())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + + ParseUser user = ParseUser.become("sessionToken"); + + // Make sure we call getUserAsync + verify(userController, times(1)).getUserAsync("sessionToken"); + // Make sure we set currentUser + verify(currentUserController, times(1)).setAsync(user); + // Make sure user's data is correct + assertEquals("sessionToken", user.getSessionToken()); + assertEquals("value", user.get("key")); + } + + @Test + public void testBecomeWithCallback() throws Exception { + // Register a mock currentUserController to make setCurrentUser work + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.setAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register a mock userController to make getUsreAsync work + ParseUserController userController = mock(ParseUserController.class); + ParseUser.State newUserState = new ParseUser.State.Builder() + .put("key", "value") + .sessionToken("sessionToken") + .build(); + when(userController.getUserAsync(anyString())) + .thenReturn(Task.forResult(newUserState)); + ParseCorePlugins.getInstance().registerUserController(userController); + + final Semaphore done = new Semaphore(0); + ParseUser.becomeInBackground("sessionToken", new LogInCallback() { + @Override + public void done(ParseUser user, ParseException e) { + done.release(); + assertNull(e); + // Make sure user's data is correct + assertEquals("sessionToken", user.getSessionToken()); + assertEquals("value", user.get("key")); + } + }); + + // Make sure we call getUserAsync + verify(userController, times(1)).getUserAsync("sessionToken"); + // Make sure we set currentUser + verify(currentUserController, times(1)).setAsync(any(ParseUser.class)); + } + + //endregion + + //region testToRest + + @Test + public void testToRest() throws Exception { + ParseUser user = new ParseUser(); + user.setUsername("userName"); + user.setPassword("password"); + + JSONObject json = user.toRest(user.getState(), user.operationSetQueue, PointerEncoder.get()); + + // Make sure we delete password operations + assertFalse(json.getJSONArray("__operations").getJSONObject(0).has("password")); + // Make sure we have username operations + assertEquals( + "userName", json.getJSONArray("__operations").getJSONObject(0).getString("username")); + } + + //endregion + + //region testValidateDelete + + @Test + public void testValidDelete() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.setObjectId("test"); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + user.setObjectId("test"); + // Make isDirty return true + user.put("key", "value"); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Cannot delete a ParseUser that is not authenticated."); + + user.validateDelete(); + } + + //endregion + + //region testValidateDelete + + @Test + public void testValidateSaveEventually() throws Exception { + ParseUser user = new ParseUser(); + user.setPassword("password"); + + thrown.expect(ParseException.class); + thrown.expectMessage("Unable to saveEventually on a ParseUser with dirty password"); + + user.validateSaveEventually(); + } + + //endregion + + //region testSynchronizeAuthData + + @Test + public void testSynchronizeAuthData() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register mock callbacks + AuthenticationCallback callbacks = mock(AuthenticationCallback.class); + when(callbacks.onRestore(Matchers.>any())) + .thenReturn(true); + ParseUser.registerAuthenticationCallback("facebook", callbacks); + + // Set user initial state + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("facebookToken", "facebookTest"); + ParseUser.State userState = new ParseUser.State.Builder() + .putAuthData(authType, authData) + .build(); + ParseUser user = ParseObject.from(userState); + user.setIsCurrentUser(true); + + ParseTaskUtils.wait(user.synchronizeAuthDataAsync(authType)); + + // Make sure we restore authentication + verify(callbacks, times(1)).onRestore(authData); + } + + @Test + public void testSynchronizeAllAuthData() throws Exception { + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + // Register mock callbacks + AuthenticationCallback callbacks = mock(AuthenticationCallback.class); + when(callbacks.onRestore(Matchers.>any())) + .thenReturn(true); + ParseUser.registerAuthenticationCallback("facebook", callbacks); + + // Set user initial state + String facebookAuthType = "facebook"; + Map facebookAuthData = new HashMap<>(); + facebookAuthData.put("facebookToken", "facebookTest"); + ParseUser.State userState = new ParseUser.State.Builder() + .putAuthData(facebookAuthType, facebookAuthData) + .build(); + ParseUser user = ParseObject.from(userState); + user.setIsCurrentUser(true); + + ParseTaskUtils.wait(user.synchronizeAllAuthDataAsync()); + + // Make sure we restore authentication + verify(callbacks, times(1)).onRestore(facebookAuthData); + } + + //endregion + + //region testAutomaticUser + + @Test + public void testAutomaticUser() throws Exception { + new ParseUser(); + + ParseUser.disableAutomaticUser(); + assertFalse(ParseUser.isAutomaticUserEnabled()); + + ParseUser.enableAutomaticUser(); + assertTrue(ParseUser.isAutomaticUserEnabled()); + } + + //endregion + + //region testAutomaticUser + + @Test + public void testPinCurrentUserIfNeededAsyncWithNoLDSEnabled() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Method requires Local Datastore."); + + ParseUser.pinCurrentUserIfNeededAsync(new ParseUser()); + } + + //endregion + + //region testPinCurrentUserIfNeededAsync + + @Test + public void testPinCurrentUserIfNeededAsync() throws Exception { + // Enable LDS + Parse.enableLocalDatastore(null); + // Register a mock currentUserController to make getCurrentUser work + ParseUser currentUser = new ParseUser(); + currentUser.setObjectId("test"); + CachedCurrentUserController currentUserController = mock(CachedCurrentUserController.class); + when(currentUserController.setIfNeededAsync(any(ParseUser.class))) + .thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + ParseUser user = new ParseUser(); + ParseUser.pinCurrentUserIfNeededAsync(user); + + // Make sure we pin the user + verify(currentUserController, times(1)).setIfNeededAsync(user); + } + + //endregion + + //region testRemove + + @Test + public void testRemoveWithUserName() throws Exception { + ParseUser user = new ParseUser(); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Can't remove the username key."); + + user.remove("username"); + } + + //endregion + + //region testSetState + + @Test + public void testSetCurrentUserStateWithoutAuthData() throws Exception { + // Set user initial state + String authType = "facebook"; + Map authData = new HashMap<>(); + authData.put("facebookToken", "facebookTest"); + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .put("oldKey", "oldValue") + .put("key", "value") + .putAuthData(authType, authData) + .build(); + ParseUser user = ParseObject.from(userState); + user.setIsCurrentUser(true); + // Build new state + ParseUser.State newUserState = new ParseUser.State.Builder() + .objectId("testAgain") + .put("key", "valueAgain") + .build(); + + user.setState(newUserState); + + // Make sure we keep the authData + assertEquals(1, user.getAuthData().size()); + assertEquals(authData, user.getAuthData().get(authType)); + // Make sure old state is replaced + assertFalse(user.has("oldKey")); + // Make sure new state is set + assertEquals("testAgain", user.getObjectId()); + assertEquals("valueAgain", user.get("key")); + } + + @Test + public void testSetStateDoesNotAddNonExistentAuthData() throws Exception { + // Set user initial state + ParseUser.State userState = new ParseUser.State.Builder() + .objectId("test") + .put("oldKey", "oldValue") + .put("key", "value") + .build(); + ParseUser user = ParseObject.from(userState); + user.setIsCurrentUser(true); + // Build new state + ParseUser.State newUserState = new ParseUser.State.Builder() + .objectId("testAgain") + .put("key", "valueAgain") + .build(); + + user.setState(newUserState); + + // Make sure we do not add authData when it did not exist before + assertFalse(user.keySet().contains("authData")); + assertEquals(1, user.keySet().size()); + assertEquals(0, user.getAuthData().size()); + // Make sure old state is replaced + assertFalse(user.has("oldKey")); + // Make sure new state is set + assertEquals("testAgain", user.getObjectId()); + assertEquals("valueAgain", user.get("key")); + } + + //endregion + + private static void setLazy(ParseUser user) { + Map anonymousAuthData = new HashMap<>(); + anonymousAuthData.put("anonymousToken", "anonymousTest"); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PointerEncoderTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PointerEncoderTest.java new file mode 100644 index 0000000..f505d86 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PointerEncoderTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertNotNull; + +public class PointerEncoderTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testEncodeRelatedObjectWithoutObjectId() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("unable to encode an association with an unsaved ParseObject"); + + ParseObject parseObject = new ParseObject("TestObject"); + JSONObject jsonObject = (JSONObject) PointerEncoder.get().encode(parseObject); + } + + @Test + public void testEncodeRelatedObjectWithObjectId() { + ParseObject parseObject = new ParseObject("TestObject"); + parseObject.setObjectId("1234"); + JSONObject jsonObject = (JSONObject) PointerEncoder.get().encode(parseObject); + assertNotNull(jsonObject); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushHandlerTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushHandlerTest.java new file mode 100644 index 0000000..eccdf6f --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushHandlerTest.java @@ -0,0 +1,28 @@ +package com.parse; + + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class PushHandlerTest { + + @Test + public void testFactory() { + PushHandler handler = PushHandler.Factory.create(PushType.NONE); + assertTrue(handler instanceof PushHandler.FallbackHandler); + + handler = PushHandler.Factory.create(PushType.GCM); + assertTrue(handler instanceof GcmPushHandler); + } + + @Test + public void testFallbackHandler() { + PushHandler handler = PushHandler.Factory.create(PushType.NONE); + assertNull(handler.getWarningMessage(PushHandler.SupportLevel.SUPPORTED)); + assertNull(handler.getWarningMessage(PushHandler.SupportLevel.MISSING_OPTIONAL_DECLARATIONS)); + assertNull(handler.getWarningMessage(PushHandler.SupportLevel.MISSING_REQUIRED_DECLARATIONS)); + assertTrue(handler.initialize().isCompleted()); + assertEquals(handler.isSupported(), PushHandler.SupportLevel.SUPPORTED); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceTest.java new file mode 100644 index 0000000..2f2615d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceTest.java @@ -0,0 +1,105 @@ +package com.parse; + + +import android.content.Intent; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ServiceController; +import org.robolectric.annotation.Config; + +import bolts.Task; +import bolts.TaskCompletionSource; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +public class PushServiceTest extends ResetPluginsParseTest { + + private PushService service; + private ServiceController controller; + private PushHandler handler; + private PushService.ServiceLifecycleCallbacks callbacks; + + @Before + public void setUp() throws Exception { + super.setUp(); + callbacks = mock(PushService.ServiceLifecycleCallbacks.class); + PushService.registerServiceLifecycleCallbacks(callbacks); + + controller = Robolectric.buildService(PushService.class); + service = controller.get(); + handler = mock(PushHandler.class); + service.setPushHandler(handler); + + Parse.Configuration.Builder builder = new Parse.Configuration.Builder(service); + ParsePlugins.initialize(service, builder.build()); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + PushService.unregisterServiceLifecycleCallbacks(callbacks); + } + + @Test + public void testOnCreateWithoutInit() { + ParsePlugins.reset(); + controller.create(); + verify(callbacks, never()).onServiceCreated(service); + } + + @Test + public void testOnCreate() { + controller.create(); + verify(callbacks, times(1)).onServiceCreated(service); + } + + @Test(expected = IllegalArgumentException.class) + public void testCannotBind() { + controller.create().bind(); + } + + @Test + public void testStartCommand() throws Exception { + controller.create(); + service.setPushHandler(handler); // reset handler to our mock + + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + final Task handleTask = tcs.getTask(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + tcs.setResult(null); + return null; + } + }).when(handler).handlePush(any(Intent.class)); + + controller.startCommand(0, 0); + handleTask.waitForCompletion(); + + verify(callbacks, times(1)).onServiceCreated(service); + verify(handler, times(1)).handlePush(any(Intent.class)); + } + + @Test + public void testDestroy() { + controller.create(); + controller.startCommand(0, 0); + controller.destroy(); + verify(callbacks, times(1)).onServiceDestroyed(service); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceUtilsTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceUtilsTest.java new file mode 100644 index 0000000..0d8b22e --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/PushServiceUtilsTest.java @@ -0,0 +1,42 @@ +package com.parse; + + +import android.content.Intent; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ServiceController; +import org.robolectric.annotation.Config; + +import bolts.Task; +import bolts.TaskCompletionSource; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +public class PushServiceUtilsTest { + + @Test + public void testDefaultHandler() { + ManifestInfo.setPushType(PushType.NONE); + PushHandler handler = PushServiceUtils.createPushHandler(); + assertTrue(handler instanceof PushHandler.FallbackHandler); + + ManifestInfo.setPushType(PushType.GCM); + handler = PushServiceUtils.createPushHandler(); + assertTrue(handler instanceof GcmPushHandler); + } + +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ResetPluginsParseTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ResetPluginsParseTest.java new file mode 100644 index 0000000..da19edf --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/ResetPluginsParseTest.java @@ -0,0 +1,24 @@ +package com.parse; + +import org.junit.After; +import org.junit.Before; + +/** + * Automatically takes care of setup and teardown of plugins between tests. The order tests run in can be + * different, so if you see a test failing randomly, the test before it may not be tearing down + * properly, and you may need to have the test class subclass this class. + */ +class ResetPluginsParseTest { + + @Before + public void setUp() throws Exception { + ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + } + + @After + public void tearDown() throws Exception { + ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/SubclassTest.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/SubclassTest.java new file mode 100644 index 0000000..a1e90b6 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/SubclassTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.IllegalArgumentException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +public class SubclassTest extends ResetPluginsParseTest { + /** + * This is a subclass of ParseObject that will be used below. We're going to imagine a world in + * which every "Person" is an instance of "The Flash". + */ + @ParseClassName("Person") + public static class Person extends ParseObject { + public String getNickname() { + return getString("nickname"); + } + + public void setNickname(String name) { + put("nickname", name); + } + + public String getRealName() { + return getString("realName"); + } + + public void setRealName(String name) { + put("realName", name); + } + + @Override + void setDefaultValues() { + setNickname("The Flash"); + } + + public static ParseQuery getQuery() { + return ParseQuery.getQuery(Person.class); + } + } + + @ParseClassName("NoDefaultConstructor") + public static class NoDefaultConstructor extends ParseObject { + public NoDefaultConstructor(Void argument) { + } + } + + @ParseClassName("ClassWithDirtyingConstructor") + public static class ClassWithDirtyingConstructor extends ParseObject { + public ClassWithDirtyingConstructor() { + put("foo", "Bar"); + } + } + + @ParseClassName("UnregisteredClass") + public static class UnregisteredClass extends ParseObject { + } + + public static class MyUser extends ParseUser { + } + + public static class MyUser2 extends ParseUser { + } + + @Before + public void setUp() throws Exception { + super.setUp(); + ParseObject.registerParseSubclasses(); + ParseObject.registerSubclass(Person.class); + ParseObject.registerSubclass(ClassWithDirtyingConstructor.class); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + ParseObject.unregisterParseSubclasses(); + ParseObject.unregisterSubclass(Person.class); + ParseObject.unregisterSubclass(ClassWithDirtyingConstructor.class); + } + + @SuppressWarnings("unused") + public void testUnregisteredConstruction() throws Exception { + Exception thrown = null; + try { + new UnregisteredClass(); + } catch (Exception e) { + thrown = e; + } + assertNotNull(thrown); + ParseObject.registerSubclass(UnregisteredClass.class); + try { + new UnregisteredClass(); + } finally { + ParseObject.unregisterSubclass(UnregisteredClass.class); + } + } + + @Test + public void testSubclassPointers() throws Exception { + Person flashPointer = (Person) ParseObject.createWithoutData("Person", "someFakeObjectId"); + assertFalse(flashPointer.isDirty()); + } + + @Test + public void testDirtyingConstructorsThrow() throws Exception { + ClassWithDirtyingConstructor dirtyObj = new ClassWithDirtyingConstructor(); + assertTrue(dirtyObj.isDirty()); + try { + ParseObject.createWithoutData("ClassWithDirtyingConstructor", "someFakeObjectId"); + fail("Should throw due to subclass with dirtying constructor"); + } catch (IllegalStateException e) { + // success + } + } + + @Test + public void testRegisteringSubclassesUsesMostDescendantSubclass() throws Exception { + try { + // When we register a ParseUser subclass, we have to clear the cached currentParseUser, so + // we need to register a mock ParseUserController here, otherwise Parse.getCacheDir() will + // throw an exception in unit test environment. + ParseCurrentUserController controller = mock(ParseCurrentUserController.class); + ParseCorePlugins.getInstance().registerCurrentUserController(controller); + assertEquals(ParseUser.class, ParseObject.create("_User").getClass()); + ParseObject.registerSubclass(MyUser.class); + assertEquals(MyUser.class, ParseObject.create("_User").getClass()); + ParseObject.registerSubclass(ParseUser.class); + assertEquals(MyUser.class, ParseObject.create("_User").getClass()); + + // This is expected to fail as MyUser2 and MyUser are not directly related. + try { + ParseObject.registerSubclass(MyUser2.class); + fail(); + } catch (IllegalArgumentException ex) { + /* expected */ + } + + assertEquals(MyUser.class, ParseObject.create("_User").getClass()); + } finally { + ParseObject.unregisterSubclass(ParseUser.class); + ParseCorePlugins.getInstance().reset(); + } + } + + @Test + public void testRegisteringClassWithNoDefaultConstructorThrows() throws Exception { + Exception thrown = null; + try { + ParseObject.registerSubclass(NoDefaultConstructor.class); + } catch (Exception e) { + thrown = e; + } + assertNotNull(thrown); + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TaskQueueTestHelper.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TaskQueueTestHelper.java new file mode 100644 index 0000000..4341409 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TaskQueueTestHelper.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import java.util.ArrayList; +import java.util.List; + +import bolts.Continuation; +import bolts.Task; +import bolts.TaskCompletionSource; + +/** + * Helper class to step through a {@link TaskQueue}. + * + * {@link #enqueue()} and {@link #dequeue()} works as FIFO list that enqueues an unresolved + * {@link Task} to the end of the {@link TaskQueue} and resolves the first {@link Task}. + */ +public class TaskQueueTestHelper { + + private final Object lock = new Object(); + private final TaskQueue taskQueue; + private final List> pendingTasks = new ArrayList<>(); + + public TaskQueueTestHelper(TaskQueue taskQueue) { + this.taskQueue = taskQueue; + } + + /** + * Pauses the {@link TaskQueue} by enqueuing an unresolved {@link Task} to it. + */ + public void enqueue() { + synchronized (lock) { + final TaskCompletionSource tcs = new TaskCompletionSource(); + taskQueue.enqueue(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + return tcs.getTask(); + } + }); + pendingTasks.add(tcs); + } + } + + /** + * Resumes the {@link TaskQueue} by resolving the first {@link Task}. + */ + public void dequeue() { + synchronized (lock) { + TaskCompletionSource tcs = pendingTasks.remove(0); + tcs.setResult(null); + } + } +} diff --git a/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TestHelper.java b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TestHelper.java new file mode 100644 index 0000000..4f975ce --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/Parse/src/test/java/com/parse/TestHelper.java @@ -0,0 +1,9 @@ +package com.parse; + +/** + * Helper class for testing + */ +public class TestHelper { + + public static final int ROBOLECTRIC_SDK_VERSION = 25; +} diff --git a/ExternalLibs/Parse-SDK-Android/README.md b/ExternalLibs/Parse-SDK-Android/README.md new file mode 100644 index 0000000..5753c0b --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/README.md @@ -0,0 +1,158 @@ +# Parse SDK for Android + +[![Maven Central][maven-svg]][maven-link] +[![Dependencies][dependencies-svg]][dependencies-link] +[![References][references-svg]][references-link] +[![License][license-svg]][license-link] + +[![Build Status][build-status-svg]][build-status-link] +[![Coverage Status][coverage-status-svg]][coverage-status-link] + +[![Join Chat](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ParsePlatform/Chat) + +A library that gives you access to the powerful Parse cloud platform from your Android app. +For more information about Parse and its features, see [the website][parseplatform.org], [blog][blog] and [getting started][guide]. + +## Getting Started +### Installation +- **Option 1:** Gradle + + Add dependency to the application level `build.gradle` file. + + ```groovy + dependencies { + implementation 'com.parse:parse-android:1.16.7' + } + ``` + + Snapshots of the development version are available in [jFrog's `snapshots` repository][snap]. + +- **Option 2:** Compiling for yourself into AAR file + + If you want to manually compile the SDK, begin by cloning the repository locally or retrieving the source code for a particular [release][releases]. Open the project in Android Studio and run the following commands in the Terminal of Android Studio: + + ``` + ./gradlew clean build + ``` + Output file can be found in `Parse/build/outputs/` with extension .aar + + You can link to your project to your AAR file as you please. + + ### Setup +- **Option 1:** Setup in the Manifest + + You may define `com.parse.SERVER_URL` and `com.parse.APPLICATION_ID` meta-data in your `AndroidManifest.xml`: + + ```xml + + + + ... + + ``` + +- **Option 2:** Setup in the Application + + Initialize Parse in a custom class that extends `Application`: + + ```java + import com.parse.Parse; + import android.app.Application; + + public class App extends Application { + @Override + public void onCreate() { + super.onCreate(); + Parse.initialize(new Parse.Configuration.Builder(this) + .applicationId("YOUR_APP_ID") + .server("http://localhost:1337/parse/") + .build() + ); + } + } + ``` + + For either option, the custom `Application` class must be registered in `AndroidManifest.xml`: + + ```xml + + ... + + ``` + +## Usage +Everything can done through the supplied gradle wrapper: + +### Run the Tests +``` +./gradlew clean testDebug +``` +Results can be found in `Parse/build/reports/` + +### Get Code Coverage Reports +``` +./gradlew clean jacocoTestReport +``` +Results can be found in `Parse/build/reports/` + +## How Do I Contribute? +We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines][contributing]. + +## Other Parse Projects + + - [ParseUI for Android][parseui-link] + - [ParseLiveQuery for Android][parselivequery-link] + - [ParseFacebookUtils for Android][parsefacebookutils-link] + - [ParseTwitterUtils for Android][parsetwitterutils-link] + +## License + Copyright (c) 2015-present, Parse, LLC. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. + + [parseplatform.org]: http://parseplatform.org/ + [blog]: http://blog.parse.com/ + [guide]: http://docs.parseplatform.org/android/guide/ + + [latest]: https://search.maven.org/remote_content?g=com.parse&a=parse-android&v=LATEST + [snap]: https://oss.jfrog.org/artifactory/oss-snapshot-local/com/parse/parse-android/ + + [maven-svg]: https://maven-badges.herokuapp.com/maven-central/com.parse/parse-android/badge.svg?style=flat + [maven-link]: https://maven-badges.herokuapp.com/maven-central/com.parse/parse-android + + [dependencies-svg]: https://www.versioneye.com/java/com.parse:parse-android/badge.svg?style=flat-square + [dependencies-link]: https://www.versioneye.com/java/com.parse:parse-android + + [references-svg]: https://www.versioneye.com/java/com.parse:parse-android/reference_badge.svg?style=flat-square + [references-link]: https://www.versioneye.com/java/com.parse:parse-android/references + + [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg + [license-link]: https://github.com/parse-community/Parse-SDK-Android/blob/master/LICENSE + + [build-status-svg]: https://travis-ci.org/parse-community/Parse-SDK-Android.svg?branch=master + [build-status-link]: https://travis-ci.org/parse-community/Parse-SDK-Android + + [coverage-status-svg]: https://img.shields.io/codecov/c/github/parse-community/Parse-SDK-Android/master.svg + [coverage-status-link]: https://codecov.io/github/parse-community/Parse-SDK-Android?branch=master + + [parseui-link]: https://github.com/parse-community/ParseUI-Android + [parselivequery-link]: https://github.com/parse-community/ParseLiveQuery-Android + + [parsefacebookutils-link]: https://github.com/parse-community/ParseFacebookUtils-Android + [parsetwitterutils-link]: https://github.com/parse-community/ParseTwitterUtils-Android + + [releases]: https://github.com/parse-community/Parse-SDK-Android/releases + [contributing]: CONTRIBUTING.md diff --git a/ExternalLibs/Parse-SDK-Android/build.gradle b/ExternalLibs/Parse-SDK-Android/build.gradle new file mode 100644 index 0000000..b4a5e00 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/build.gradle @@ -0,0 +1,32 @@ +buildscript { + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.0.0' + } +} + +plugins { + id "com.jfrog.artifactory" version "4.4.18" + id "com.jfrog.bintray" version "1.7.3" + id 'com.github.ben-manes.versions' version '0.17.0' +} + +allprojects { + repositories { + jcenter() + google() + } +} + +ext { + compileSdkVersion = 27 + buildToolsVersion = "27.0.0" + + supportLibVersion = '27.0.1' + + minSdkVersion = 14 + targetSdkVersion = 27 +} diff --git a/ExternalLibs/Parse-SDK-Android/gradle/ExcludeDoclet.jar b/ExternalLibs/Parse-SDK-Android/gradle/ExcludeDoclet.jar new file mode 100644 index 0000000000000000000000000000000000000000..0fb5f89dc52371d8fbcdb0ff58c32dd781233fe0 GIT binary patch literal 3539 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 o1_w0`8j?RXMrrt6cqqKIJAHI7g&)0;{X5v literal 0 HcmV?d00001 diff --git a/ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.jar b/ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..db34964573c2310896266cb70b6f1d378bcb3213 GIT binary patch literal 54783 zcmafaW0WS*vSoGIwr!)!wr%4p+g6utqszAKsxI5MZBNhK_h#nax$n)7$jp^1Vx1G2 zC(qu2RFDP%MFj$agaiTt68tMbK*0a&2m}Q6_be-_B1k7GC&mB*r0`FQu26lR{C^cx z{>oqT|Dz}?C?_cuFbIhy@Hlls4PVE#kL z%+b)q8t~t$qWrU}o1>w6dSEU{WQ11MaYRHV`^W006GEHNkKbo3<`>slS- z^Iau?J5(A*RcG;?9caykA`<#qy1~O zV;;PYMn6SI$q}ds#zKhlt{2DkLyA|tPj@5nHw|TfoB{R9AOtjRH|~!gjc7>@`h6hQ zNQ|Ch4lR}rT_GI4eQoy|sMheUuhTnv@_rRPV^^6SNCY zJt~}LH52Y+RK{G^aZh@qG*^+5XM={Yu0CS=<}foB$I}fd5f&atxdLYMbAT-oGoKoE zEX@l(|ILgqD&rTwS4@T(du@BzN3(}du%3WCtJ*e1WJ5HWPNihA7O65R=Zp&IHPQn{ zTJ{$GYURp`Lr$UQ$ZDoj)1f(fN-I+C0)PVej&x_8WZUodh~2t5 z^<=jtVQnpoH>x5ncT0H=^`9-~oCmK=MD#4qnx+7-E-_n^0{2wjL2YV;WK(U;%aCN} zTPh334F$MTbxR7|7mEtX3alSAz|G)I+eFvQnY}XldO7I7$ z2-ZeSVckL<)N1tQ)M6@8uW;`pybJ4+Zf4&;=27ShUds^TB8DN4y^x=7xslL*1%HX_ zT(iSMx?g}!7jTEjX@&lI{{ifXnD}tWA8x4A3#o?GX9GMQHc-%WBBl|UlS|HYNH}JU z?I48Qizg+VWgSZ#zW<;tMruWI@~tW~X_GT(Me0(X0+ag8b-P6vA(1q165LJLl%zIl z?Ef?_&y7e?U@PK^nTSGu!90^0wjPY}`1@cng< z8p@n!$bcZvs3dwYo!t+cpq=9n`6Gi|V&v32g3zJV>ELG|eijj@>UQ8n)?`HPYai20W!}g}CSvAyisSPm0W|p?*Zq_r(%nCY8@}OXs2pS4# zI*)S^UFi`&zltazAxB2B_Gt7iX?Y25?B#w+-*y#dJIH(fIA<(GUhfiupc!IVAu&vF zg3#yzI2SrRpMSxpF*`0Ngul=!@E0Li|35w|ING^;2)a0%18kiwj18Ub{sSbEm38fq z1yOlHl7;{l4yv_FQZ`n><+LwoaKk|cGBRNnN;XDstie!~t5 z#ZWz9*3qvR2XkNZYI0db?t^(lG-Q8*4Jd6Q44rT71}NCQ2nryz(Btr|?2oa(J1`cn z`=-|7k;Q^9=GaCmyu(!&8QJRv=P5M#yLAL|6t%0+)fBn2AnNJg%86562VaB+9869& zfKkJa)8)BQb}^_r0pA1u)W$O`Y~Lenzyv>;CQ_qcG5Z_x^0&CP8G*;*CSy7tBVt|X zt}4Ub&av;8$mQk7?-2%zmOI4Ih72_?WgCq|eKgY~1$)6q+??Qk1DCXcQ)yCix5h#g z4+z7=Vn%$srNO52mlyjlwxO^ThKBz@(B8WGT`@!?Jhu^-9P1-ptx_hfbCseTj{&h}=7o5m0k)+Xx7D&2Vh zXAY*n|A~oM|4%rftd%$BM_6Pd7YVSA4iSzp_^N|raz6ODulPeY4tHN5j$0K9Y4=_~ z)5Wy%A)jp0c+415T7Q#6TZsvYF`adD%0w9Bl2Ip`4nc7h{42YCdZn};GMG+abcIR0 z+z0qSe?+~R5xbD^KtQ;-KtM$Q{Q~>PCzP!TWq`Wu@s-oq!GawPuO?AzaAVX9nLRvg z0P`z82q=Iw2tAw@bDiW;LQ7-vPeX(M#!~eD43{j*F<;h#Tvp?i?nMY1l-xxzoyGi8 zS7x(hY@=*uvu#GsX*~Jo*1B-TqL>Tx$t3sJ`RDiZ_cibBtDVmo3y^DgBsg-bp#dht zV(qiVs<+rrhVdh`wl^3qKC2y!TWM_HRsVoYaK2D|rkjeFPHSJ;xsP^h-+^8{chvzq z%NIHj*%uoS!;hGN?V;<@!|l{bf|HlP0RBOO(W6+vy(ox&e=g>W@<+P$S7%6hcjZ0< z><8JG)PTD4M^ix6OD5q$ZhUD>4fc!nhc4Y0eht6>Y@bU zmLTGy0vLkAK|#eZx+rXpV>6;v^fGXE^CH-tJc zmRq+7xG6o>(>s}bX=vW3D52ec1U(ZUk;BEp2^+#cz4vt zSe}XptaaZGghCACN5JJ^?JUHI1t^SVr`J&d_T$bcou}Q^hyiZ;ca^Um>*x4Nk?)|a zG2)e+ndGq9E%aKORO9KVF|T@a>AUrPhfwR%6uRQS9k!gzc(}9irHXyl5kc_2QtGAV7-T z+}cdnDY2687mXFd$5-(sHg|1daU)2Bdor`|(jh6iG{-)1q_;6?uj!3+&2fLlT~53- zMCtxe{wjPX}Ob$h2R9#lbdl0*UM_FN^C4C-sf3ZMoOAuq>-k+&K%!%EYYHMOTN~TB z8h5Ldln5sx_H3FoHrsaR`sGaGoanU7+hXf<*&v4>1G-8v;nMChKkZnVV#Q_LB{FXS ziG89d+p+9(ZVlc1+iVQy{*5{)+_JMF$Dr+MWjyO@Irs}CYizTI5puId;kL>fM6T(3 zat^8C6u0Ck1cUR%D|A<;uT&cM%DAXq87C~FJsgGMKa_FN#bq2+u%B!_dKbw7csI=V z-PtpPOv<q}F zS)14&NI3JzYKX?>aIs;lf)TfO3W;n+He)p5YGpQ;XxtY_ixQr7%nFT0Cs28c3~^`d zgzu42up|`IaAnkM;*)A~jUI%XMnD_u4rZwwdyb0VKbq@u?!7aQCP@t|O!1uJ8QmAS zPoX9{rYaK~LTk%3|5mPHhXV<}HSt4SG`E!2jk0-C6%B4IoZlIrbf92btI zCaKuXl=W0C`esGOP@Mv~A!Bm6HYEMqjC`?l1DeW&(2&E%R>yTykCk*2B`IcI{@l^| z8E%@IJt&TIDxfFhN_3ja(PmnPFEwpn{b`A z`m$!H=ek)46OXllp+}w6g&TscifgnxN^T{~JEn{A*rv$G9KmEqWt&Ab%5bQ*wbLJ+ zr==4do+}I6a37u_wA#L~9+K6jL)lya!;eMg5;r6U>@lHmLb(dOah&UuPIjc?nCMZ)6b+b4Oel?vcE5Q4$Jt71WOM$^`oPpzo_u; zu{j5ys?ENRG`ZE}RaQpN;4M`j@wA|C?oOYYa;Jja?j2?V@ zM97=sn3AoB_>P&lR zWdSgBJUvibzUJhyU2YE<2Q8t=rC`DslFOn^MQvCquhN~bFj?HMNn!4*F?dMkmM)## z^$AL9OuCUDmnhk4ZG~g@t}Im2okt9RDY9Q4dlt~Tzvhtbmp8aE8;@tupgh-_O-__) zuYH^YFO8-5eG_DE2!~ZSE1lLu9x-$?i*oBP!}0jlk4cy5^Q;{3E#^`3b~Su_bugsj zlernD@6h~-SUxz4fO+VEwbq+_`W{#bG{UOrU;H)z%W0r-mny1sm#O@gvwE72c^im)UrJnQgcB_HxILh!9fPQ);whe*(eIUjA(t{8iI(?NY<5^SGOr;vrcKpedfTu zWCTHMK16<@(tI%`NxN3xW6nKX{JW=77{~yR$t1$xwKUm7UJmOrnI4Z zajmwO&zZ8PhJ6FNRjID+@QZ8fz%%f2c{Xh*BWDIK zXrFxswPdd;(i}fLsNVb(sx-hMJ>IQ0QvH^z3= zc;TX|YE>HpO6-C5=g{+l3U6fF`AXJM6@kcoWLQXxiNiXab#!P8ozeR^oy#PfdS#aj zUDKKNx>5&v%k*OBF;-)X5Afpd60K{FTH@1|)>M!!F)jb))f&{UY-rcR>h z`~9|W#a`Yw7fD~{3`rktJC|L46-(sRaa~hM-d#KSG6@_*&+pnNYQ2JSy@BNg_Tx7< zB-vhG+{d^*zIH!;2M7O`_S{?EKffQ02;N>=2!3JqQX(M_Aj#}dCfdb?yGH%tk^_Zf zAtZ5!rnq4(WSd!_GfuPp4uDd2(8%>)Iu6z=XjRQLi2_RBg97~ zr$zf>FNkUG3~bp6#hl^3HSA2*SS-DT_QkX#QNcG2?8&Cm6Sj#}yaqEhjq1GabS)ZwBhcKc;52~Qc*Z@=jRjfqZO1%y?*D(iB&EE z-Aln~CD}?DqVGGB``Q@F-TY|Fj7)4D28@Z-@a-A4(KC*}W4*2l?E>!wviGFcB*Dc3z50hH^i0Y`j zip{Em#(a42NnOEvkU+6SfAkEzO$ z*j*3sOP4y2W@t7)nbi9Dcj|9Bw}z)VzKuAx4<&3`!gMhuW5&4%F@_!ZKBoaBHYwcn3WcL^0l zkdkY#l8~$5UazRWOJo32=kA|tKs!Y_vX=+xrA3Mwd45^vZe02+dI_r|rmO-`>l0$i zEB%YFf8ecv=Q@YPntwR)df$>p+zI@!1-aj13HMYz5$QWWp$U&Z(I?C5rYl8S=m|d!*(Y&`gzl zu00=P^fRg?$GE2+$)wr(ohep`G%yKT(qdGmR!M45W`~K4bC@YwX{J;T@dq=$9o>;L zz%NIUoFhZxHIjtR1kdw5V7u=4{!3oQc;za?0UQVj5f%uD<=^`&>TYc9;$-0p5VNob z2pSvzby?QX*3j%fJx*5BcET~k^5xT{iQin-qP*nWQ9THOA69^wDN5utzTj#~upjf}CtShX9;wdXE35EVlzWqIGJ z)io1?vG_sea+iQjU%m@q)4(=eS5zC1h|!bCE~d9gvl{7)!IScau*OTR`)!Mhr`mdX zlhmcf-Ms-t;DYx9o2z=q68Nm{ zOF;j&-eqWvD}_5X8`^t48wcrR%*&RycEe!J5nJguNo~cP6)1|!4@Jb2YL6IYdyrH8 zI$W1D+$LRa4*EC=4Cr)=0Qap5g}M^+jyvlDE}G8-wsVQYX&UXR#=~{XZLTPY`=3=N zkvaUS+4ofuBn|356>5pTPX|r)^QG(R2d$TX>Krwf&QVgVCM9zP64l%Z8B=2RYP%{E zaKc@qdtK`R({$|K`t5>0?KorZI1)6`9@|#O>v1WK@3bbLFtGM4gd98X0(-9{W{NiN zIuG0D%0l5WhXSRNbfROzH6w*YO&2Xpx5amm%+T4$qtvPDK+eUjfs$g@<`DBwNH1(33NhDKwO*I9E z$bW{D7h4@U~&K4klFtk`+Smzy>$vNph6hQsYQ1QF(- zHK>f)>|MT%=q)(U-3br5R4KIE!FeeTP`{-^wpgKJzcOqD?!&-6Yf7fd<^40T$r z{@91>s^KAH@mw(72{v#n4rzh?z_qh-AL;FAt==sT(BFv)(FXSoKd)RMA40`^)3^+Z zwdPe9j*t}}%!Fk@58lX}s`NX-7M;>k)w7j1`*~g_dAMDLsOq`@C>D(lreX%!c_OjX zTP$xDO*C|S27Hd)6?;6;Y`P3$%YFG)9y2H0Yuw;6Z2{^y2YvKP`V&OVi;L`j{L;jL zvz-omEQby(t)f?-HssRfTDYnS`=UG{>1Y)Dh(Xb>WU++>XOoF@TR;-#<1E+1AqPdk=H6)VQ32z zLdHM3uv~8{(>v|*O>k2VTW}=fw~%fuNfyf6FMaEXzdHB?tnHs6%)R(k_^``|IN|L# zV&QQG*x~n}a?;|la|TQD383!6WOfCv9V@-(g`ab3{CgpIjQ zGyCjpiIaK${m-Zd;m*k+7;?~M6)Wqb>yI*k`=@zOr%NjIs(C?BUqCq8^ zsi_)Bk)kyU`NL<6nholj+3Xs*E%vZ2H<};VoFCvMFLYwFg-gi8C%2@0gH#_lU>~8E z?>!v9-YFw6r=Z{xMI59a3J6_y8&}4UeEr?9w($B){={R9reR;r4Jgl?G)eMv=EOsc zckWsS;fuDu;l?Dgzgyhj^H>RMJs^*kzUfB#Ax}fqmj?Eb#G1W$J(4a)qfI(k=2*_Y zqr3?H*#`c8owZQ>48MUl@A(yQxuXBM2|bdy`x=bcfHc~8b9#odFy|NGMC(oMC%C+$ zi;L=xaJ%=;6Qf)kX-netDG|g#BZrnfdTm79e(Px7oy)wLHNB^EUMI7snGBJIuq*RP z@Xv@1TIRW_^S82~__wm~U(}t&|5uS))d}DzVP^x7v9q&svHy>{v$D24wjk=4SiJ7i zqf#YhQ?sQusP?MXrRx0PczL)ABq5Z%NibA3eTRvr^@n;Fsio!I2;YM^8}EP;&7WT# zqivIJ-A+dn6W9FwzQ7v&<$;P5qwe`TR5_AiRFDRGVmdG3h+?&byKRASKwXHQiegIU zvi;If(y)ozZ%=Q6)cR|q)pkV>bAocyDX#Om&LQ?^D;#XBhNC;^+80{v1k1(4X1RWKo4Onb+)A zp&OGpq39Ss9Do68%xbC+SH>N@bhr?aF^3ARMK)^mWxfuvt|?ucl0$sf){gT9_b~^# z3>QnE)-@zE%xH=ax{R1+8?7wHJFQhqx1xirV(lZN0HU=>7ODhQ5k^5BK973IumdDP z(oUtiC^Ya#Q@9^~vNuH)*L|F$!0eySLZ_2FYGn%S71MQAFrHK4i#UwxjM0gxL;pC#^nGA?B0S zjI>+f^}Ik10y+Dkm{%iS3&XUVZ;GCHpJ5Re31~x@7X68v;(n<6>>q?g=^VldiKw#@ zEOQ_*7zX;nDQmDM597=8yqlznk7 z+#rTK!TN>LKK0vPkO?^!tGYfh{PQwx2{$;;hXw+o#{4V)o@o7JnX3Pzzv6$kNc=~k zLIc7ZWf|+6KhEdwl_w5PEQknl2TTo9GE7ziZ{5ESq%({Nit}IqJ>FT2iz#C<-kH>9 zZ7#i0)@|N7p)q-r1L{;J^UC?UYp(10rKh8TRyy>yhJWXD>$&^W=lZ>SB=Othg$XEg z5FL%%z9nMPJzPhRIyIGwqaa@*F!II`tmbAv*|$^bO0Q~(jj|aJj5BP6N%o zi>Fh52P_qg$2UE^&NabtBe|(p{jB`_nxYv`c#kx>LN*OSN+N zU4?c;6AYnTgQjgGHWamUI~Jj|bO=J#gpsI+{P2#bjpt${i6FN0W?!+*Po|F(Ep~r^ znlCW6`~{P*dJn~2sE-28TWaVhPubr5OB6wFGHdSr{ylUzA%71gLT*B+enM2v-TrvO ztop}Gd0>sC_EpOG@@K2?m+wHVUHJ=ochwHJueUm~pZw7CElAsk!cgpuF&clLJlcoM z5RfmuLPJGOQ&+|Qje(!|_U>laCSIu5Go16&6C`MR%qhi#y^MTR$a|FuE7KaW!jdVu zQc6y3$b-fjA|zT|iyLgCtE)?+*{ez$14G@qDry0u%fYe=m_L9 zcpCG?q=Z0|3N5rQ75C6%&qtH`V%gd}#f)a{GqGaN!;vg5_;5m_q=-%TK(QnPrSGBM zJR)n3VvZ+adg)`v(iogiMOEgsJRqsAT%F)$7q%>N z+>ypdC#5P+#5I)8tD%Jz_C$CkQ4(v+;XO+*-@Vqfr%y4;NXBbf)IKJp+YrDNXQtxD zPjcXDE`uD{H50-$)3Jxd>X|xN$u3~#ft_j`y+MY-5bs>?@)We6Dr$y%FUB(3ui3I# z7^>}aXe=hA%0I;(8>2ca-1`OXuRv5Kv8h?&2rUu>D9D7L@V+srE z;`vC7L`JG;GbZ`e$0uDdeHVMFNI+5qBQG04|Ejy-g zBlav6v%&NUA^JNO?bO@ZQP|(AT!lFEgBu*fg)=wOA5wiaY#-n~WK#|S`TM7(g1I)Y z{MElhws)Vgzx?^BUlK$3_Zei$(_xyl<)dBB_p!esdMsYJzw(HJx!JOYS=cmMrTh5V zK48AlHI8<>h)vH(Dt}CkO2SPKUCu>*r(ZT(MEJC`EoDeyIjAiZ z4!$#Bv;#Ha|50x!E~2$H@qVM*{HX?6=U`;C_*DY9J?+_ zE_1(oZky$GE>%urwl$tN$r2Q;P6h=-(#J>KqL@4-5)GJp?Lnl!QHTV56UmG?h?t2t z8N0+xSbWmtk1G4%6cSek>wX?&<^~ckAjopL$THKk$l^NQSZr`^P^wN!3f97?2^9l& zo!!HDu5GNryHQMMV&*B02#4$-Kd86@R8@jPjIwC0qR`5yN~0wFF<)(m`Oe--meLR- zQ^9g0Oe9t;I$nX*0sl)jqI6z_x7yg_iIO2oCo`RV(;7kceK2{MG}=Z%q=5WqSafGh zp!GmTD`*RiQDP@S%N*1(9eILhgEc~3nujB!gK^;UZ?|@f%BqT7`F*;dx;_lgxCloE zv)sDk$CT1t^!Ia2yo(vQvLn$!E<}s<-iI>wtXvs#cScn-lpVpte^S&<NYtNP%9=Z+{&Er+rD=2JmitU_vutwn0S4Po2dU$b)6jiBdJ_5VEwz9fT28%;c zk9W8e_B3!WT3Yoz&l)@3uIZ7)GxE z4Xl;;y6~Y|bC|KGj+Bzc?zL66dWH|!>z2pjQuj2bzisLrIDXD?MOOKv{oZumqO&Tt z(~hW<7OR@y^~R0RadKcc}NKI%CiV=eeh%``Vo-RnrvWK(sOydLoK zU$2g-d)ye45;H0P3=L^>a&{%W>(CZNGqYdWEauKGS;tJg%qiCob8E(^&Ltqv)pJgJ z&&ALyxTw~=UZJ1wWa6FTSiq|!=(n^Uh6myUWeNhp4XN3+{UOy#Ftu8-K`^nJ>flFd zrY{FgM8K$1LqQ75sR1Gihk}T(Mj6_MzTTVM8c=aWC@_Nbl|mSZWE8KFmDj4&kDogj zSUoIBdvUaPo-Qjs?4qPLIBoTo}E0mu%O#i zjm2g)0K=|B!>PrQU6C)*{U!S_iH;eR(+_BcTepYExFxn8!O{tLGH>!>zj_IE7r)%$ z?Kj)U{L~DD5_u&9xkDs~GuDvcMA#7<3~M4F-;4 zX{_?jDjL0nedG#Aj2fZRjuBw*dG&M}z$K~y`=~0SC{f_vKrGD^_#{2q!p2xg1IciZ z;6wviQw)Z0Hz~1MKn_K-%}1{7iCGmZyCb`R?p&CxP^!0b{>qsgub#@fpls6(4F0Qt6oWd-ZU(qRseeZ6RRT3Iw%y-mKV?})8V^t>+XKZ0#Gsb%{m&C+Up z{YiPA(cio~45i}`!<+#^hh^P^Ax*|;Uv#Z_fvLAL!yjHjeiP+X&0K}j`c_F-kh6dt(*W7~Cd0 z!!{rP?PE89LfP-8j=XH)`|5V2_sAlez76p+Ax{`9SgVx3_Iv1IRK>q9QHADt#*Y!6r?w zJ5bTiaP7*l{|Znqg@Z$x7oV~vxDJT69J;^p?pH^8117H{G^OIb5#ko3+BjY7nwHaj zt0PiK=(W2l&_CZ%!Nyr& zk;xb^^2gea?J8Y4B6V6KpAUV5{4>)%zR++g|I2XK{|fQHXS$OA+0XV5hAa9vXWGvQ z8}dDIdW4G939a{NblX`04I-%Upx46uQ;Pe{nJ*K9pf?nmI~fadH1*^4-g}b(2>rzC z#1j(IH=l-#O&&7wl>AtIDv5H{5F=QBj8)rADX4*jNMqATF)3Zm41sst%ZI71^f^ed z@k4X+T)1B&GpQ(qLaBD_CLb|`4ZHuwn4wK-^(iT`l{D(B;7B=Cz+M5OEeKs_+(z2v za^=DLy4UYtJk74ad|CLLJpGCAUwdln3G6T`G}oWeH@cHs@7q zZ;{{rJ#XqSrPu5YnVZ%rkVhU*S)AM6sn6cq+}oTU@7p!q;08Ef&9K@xt*``1yTZ(v z%rc{K^2CvW;4I;wa+Z|j@gjog^LHj>_EJal#C3qQ_`di)StH~kQa)IQfO-k@l#<%^?z_se2)nkaRm+p zPBWe7uN31~FEskXR3)9XAlHgFJv&e3NX2J-cgVY#7?_b>+!ly6f_$nIfQU#xA z)62KU z9-k;5Ns8x>h4*lKw`SPB)%zGPMKSuj^&x*-(Xe}F9l#p6%3I3~#%Xiyjwj*-4 z0~Yjnt=EbfR5^w@kvUvtQg^rxvBzS5v7#6s+?%HBy3@SdU!}ZTW!kVhx|rdZMRylS zPGddO{_KC~f7)30WFCU)mud)b&HQbnKg_k(OrbtShyJUPo>I6flvXul0WOo zW2?G$1Uv2>>~5z@7{AQS`WcR|NK6bR_;sX1TdBR4HIPQ|DWOhW7ypB95P59D(C&M? zRyztK7nufK3Uj?YTb74wuIqBT@@h!Q(R7V6Hskn&_zYAT@5l$Z;abhWF*eh-9wum8 z_WpLonUYWAz1wt9i7`t!CUb`e%cm&*bV4YBo( z58L?ql-giN`#~)zhh5Di5A(0|5>v+e9az(x%FcH27o0(St?R>iBxiyBPNoJAbZVz- zS}tavhAJ0kgd+tZjT;&?Bc%%F3vsl#+)G2N?I|@T%6`h|7*kwkGqLte^qR*n0c>>{# z-gTbvExPb@9s2(0T|wq12+Oma8+`3o#BvN+W|Q7o0p`?NLu*jCe4%a&DjmuyCl!0} z)T$0ghCzsXXT$P*~yojBLuRMs-L)E+45g0MNcMtTz>~WZ3Eud|o zf=UioWFpEiNfFa|W_xpfdNm#~s<&6v75(lXw}-{(>=qfJ=7WlEcCAs3Z&jRxGctHA zZmsbixM5%p#!f2}I@{dw5xVdzM2kMSR-8{HvT~QixsE1tq#i1Sp~a*5#|QXg@VbV{ z+l52hbp+qNh+n~mP52NCG@b03k5R zC8cEEGUo2RP-wCS{xX60P~KP3;tdynQ8QG+Bh3&#P#3%$p-jg&JZP~`lZjy-ruMup zxin_e3%MS~+@&N_lp5}Miq9Jn3IW%TuVqgu%fG%ueu!E8J<+ktfppS?F!Jjabc>)f za}Xj8`o>RnXqxrq{a^B2;5Gyqcz=Hxx}X9ABK$AV{~wt6zuR!VRSui@DOl3E({%_z zg)oTn`%0kcqqzPOFmvo_sGCzBbx)~6PT^gT9~qPTAUb1!ALaXwua$Ad zN*U$e)koOD$L}5i{V;&xe4xqwp}C&HY3ai@nL%FV;VEbZrsX$}HXikZ+tp6y-s79L zADxR-ozw#3y)ed)bF32cl&ESj!S^4XVxAeOeEPf7FKw&SRz(G50>^h;7E2H>z+1oV zt^Aj6-1+U2j>#>`fjiS%D82LgZI~_o-o9-HYPu1HwnI>;xUt!d{OlCwqmM6^GNco* z*{HS`_iuLS$Q|%q`rM$pb3Jrm$H`wT^4+4E4ueEd7&{N2QcSYVU3V?;)u*R002cF3_eFPTkdWg8D0NlE3DW8Y&l zLU9lkf8tPHl}rp2GpuEgek$~~Vhi=KV?dlcPe|`3yW84AG4T| z?>>1gRzk%lb(s>@r8GOn<9X419ydKlrh;BfB~LXh?nQvf+c3Fs1c{h-jV`hlKR9C= zznFgMZ)QnZBBWp&3nQiCAWj4!wVxAN0zAT4Wfrklj?4Xq)D?F9+M^wdt}{`YHnBOp zbKaxDALj*|g~Ged`KrVnRM9=l$lNG$tOd97ux9ljHfr-X)pox68%w2U=(bcoe7TO5 zQI^7v~qkOC9lph+Umgo3Oo#A}sib7A3lAmsx47{b#ifMtPr{^E3FN@Dnx2o=3 zK0K0Zj(MT|1o^s4@8G-(#`O1a>UatC%i3UqR#H{Jp#9LOO{~JqZFQB^gNa3VYsxxP zdtyqba^lb`2!*C;yc5UR@9C(w$6Cs~x&IQ)Jv|mm?~<|Y9lLUGjBDjr+ivj;FV${& z)>i#Ph!dL&;DJbXQsWe)MV8f!(}a8LV4>AuA#*)RBRxvoWt2RP4d}d&MphE^Iit@s zQ=^7xY2XTYwqn<gekKI^&oubIG!&M(Ua%z=;PCjAK8WP*cFqgoJZzsP4M z8~$oUsx7G6u+aQmIpAc1J-dp=*ekVHLO=1t>wfADn^aA)&}=8++o`xr*lcWERK6-w zHDoIgG2LU4rZ0t-W@&_`b5B|mi&^~DTH&scMO|Iw1{g;c?D}>#m}vZrV=dchn8!2+ z+Qv8GTIZe{$2hfQAuSh6T+7fxb2uz0%n?+)-LzU-C<}5CX#k7CplPZW{u%53Y#e(1 zgo)6_A*#Y+z6NE-9Bf{3Ib1TSl+kG;W`d(aNY+)<5Vum3Zq+4a9Ms|}*jn0;WCC64Pc1Az`CY0=-k z$5a8Mp&njQt{&nuwl|_^xS}rh< z(#wu{IlD&m3s~${!pJ`S3NM_=xyK-}pyn&Oh^$|V(F+2YB!gTUyrPQIL|pi2e$ECE65#dDJO6vV9H15{cjs1lOB zC^?*8U0M?f<}yYxI}B({nHh1AN$&YvA!~An1b64q-x7xe_c+wwLED2GHOk=SAL!pI zhb^yo3%{$IVx@YHbE!U@lDE;EKLWR4BEXg&hQdUmZ;zv#9@HatIge>B;(iwog{ZTBnlla=sVbuf&Zl_nR7(b-rg z9Cs#mA_^>qksL|9ffWG?>_CfSGLl?|b9Bx;%i*&nSc>sV96|2Ns!^cD!)+3LFN#k#g)ns{t5+U&%Ms}^M73|+A zbWC=7VIOTijqqmt0>=9~FF@Ie5_RS<=8*6W`wp5_0kSict0+sfRDLtNy$cv};X8D6 zi8u-2BrJ(O(rI=>%dq+>sL4Ou_9jF3rBWAdMgne-xyMf(JuN<0Uen)`$M(<9es0W={!<7Cdyoqp$s1~=0VWo7)M2Q_`Crm z`oa}e<}MB-F0%@=Pim~>2T3HQQ{A!KB%cbH{Rwzii0h}n&xs~)G+h&<*(YX6^pV=s z=iXu02VzEU0VUl$ZK+5C>&y56V|tytXc6IdgI|zZm{UBTgU`AKia^r1B=hbN*uCZr%c0{KFd=ZsujjZ?ux22_|-_1O^t2p9#E6B~q%zEOKL{Mp4_~2@Bhs2G?54*u@?wnOT4m3FhA`7miQhSWp_ECr)&nUh}!LD^_-DaYi;4 z7EIO+2I&@VZMks~2k)A9dz3Nt13U1+_DqiN>UIGoMR685eoV{4@BJDUod46Rv~* z;2Yc>fggVa2`16!1Q-I6)rc(qUG(9A9h(~7wDsG~AKJ?4kg04b^vgkT8&TGl2H`ER zEg4PqmkO(Za!%2nxY(#BINrEm8*;tctaEwD!MzRVGRFq9V|8K8te!-YwAt+PDY*jF zj8Qw*)1!e6=cZ7LaKq`$J$yS#!_f@v8~B#@gKXuK(V?!!ulw=>1ok`z|M+w068yZK zHKL3qH71F9Z64_^6qpk#KO5V4b~A#>Qs^W2nW&;I;%nWJFD0yrM^wSl^!HdF4Nidu z%e=#jWYSo4V!xT^i7r+@Vmz3)h>yr>E}@deBd~jL^O$GbF$8L`dx(<K}aSo)AW*O~MMc&DIKo;eE; zmpQTpQE-=efHT$a5)gC6^`LBp8|2FF|H0Thz}D7p>%-kOcWv9YZQHhOW7oEA+vcuq z+jhI#em(cR7w5g_|K%pD$x2q!q-%~j#~9D=0hq{G!M!=ersQ*+ZsJtxBS$-~h`^xU zBG3a~VJcsT885b&cEJYYLzv_T_6nUStVtHnd@F+}-P9+DrI zIsn5g30?!p%oU)QM;Q(a8mNb)$UF)rnpF>WfUrZY0}QuBjQ`gDiLy1N*tGtG(fRjK zK%SKy3=(8%xCo`BtHUnF+_Xi(|M7>@3?86PPjXja2&F5(X)+>OxXQXsxyrgbS5>KO z(mN3aDm&RNW@c_THOr9mP=c;A{SH1R0X~jjXg>|^Q!8{E;9}cs#1Gb+!r)c{JU&Lu ztzQSkpTUA`h&%2M7&u+mLFZTjP)i_tpYROxc4p%VZ(G&CgP^ly3E6* zY`KA{1$@?y_E&kh1M1RSK=%&~AI`EQ{%yoYf{<@n14#UK4c5~nRmP6A+_}li5eh|- zCj3$h|BmJfR%p`C8-?5tA5Jk+MG$U5(K;UryU)s~_S2iw=bL28eq*Fc$=6v}i@mPQ z$mh)Lfs@y6>owe+Yj%$<@sd9{tp|Bugm`CG2jPN(N*gNjtq!qM>f_XcPBt0W=H-_6 zNYw%7kmtK>FEx42u^3r@nlWBssyVNJa$rNqpyxBwsVMHg0zIJHGvNR&aPe6_&!6F2 zm}BNUTQm56;Azu|VG=1e8uSfo2v4+>RV{r1B7-IMPySp8{9O96RuAGXjL`p!`rSNy zz=cxhK5IEb1E8bc>S$e*F{Q6R;?@DY9Th(x7BA-aJ^cYZm=&rb{aT0qho@fMd+q5) z3_9!_fsi-#QH{Vv3t_(}{P8kgw=JL4wcsF^9~m0}2W;O~%+3eB+8dpLA-EkEBwjbz z&d1MMgzYDQ%&yR3)DvN~4-6|_+S&1)))139O22&E4JnT#oxl`JbJCAkosbmV{tevO zm|52qAJ2i{CsFiiUm@N)Zr-r1!RxH%VA~l@mPW?|2FfOTo1v6mAC28;LZ{J!LKrzu zM`8UDfM1SRC0f_~(|uAW$ZK5DfV|UlNV(P&a)cOC_GE=_6-?P%bpsTlHsgw3IDUx% zlg7v{TuS?SHIJ2<>S5A5jSiSPNsOp~x`78tFb6-!94&v2_bf=+x%Y91J)J5m?ut{#oW zReUZ~yW+En!(CwK%dB3vV;MP1daw|2W4g5^>PKe%+#qaGtTR&}$CW=};G@rdn8g29 z|8ZLr4uhW7^E1c;0C&wLfxm%{BD9h|&$EHOjOIExebr?Iozk2>tlRQ`%?i$#ak9|O z%bX>DK;z*`XghIR63)B<4V~ihpTd?7 ze1dD>7F547l6gmZy~(B#F`=$sf<0iaxNtVFZW}ZezI35;UV&6*MH$kTLS8_|X86LE zC8NH}wIN|LF<}j+YK!2W){|D@^5YfV<|oZsj@h1VA$MFzv!K z8LGBZ(&N`oXh3-6cB3>#S)2D7A_<=(ZPz|YcOaGLD^0I-vaP@(kC$&%oYn<0_$Bcb z2N{RKWvo(7MB+ME&e(?^HS`6cJwo%8wXxUJ$2YaNri5^_dKmIT7me(L@LKT&(Tz%H}F0D{FH@c0}ar2*hV4 zOnWnJf9fb<)7>=>BkrEzaFd= zxzn|){KI|-1ONc{-$QFswx<8Z%m0<|ZaXK3G}4nYLQz9MY$uh9m<1`U8f;5X5^Mwk zj|*W!@?MpgQ7vhnhZOY{?)wX4Xb|@g(4T_H<7OBHwT9U2Z?6RQoO=r2&(AlQ9XQzp zu^kh@6gx`)^->b~Kq?{aP)>o3Bs)C*xEa0Bm=aJ|^c9GKHO2vkjbrG#Gx5t*9c#~C z^m^@qy_%8%9@nih?*ti^j^^U@k#a+DPPWLllHs7dg(ht6S!`!Lhr@z`Xps&1_U3BG zk|8)|>#RJv%j_~-r6DD1?bEhs{Zr~VIgGnep~Ws}%AZO(e(FHM!vK zW>FnpNBi>3Bdx_#2<0gu57L7;pt3awsigs|8nPhvnQ6GTC8kz9l&jU4gS@vpG_M;* zJ|)`a^b6Aa17arkbQNj8&{rh$0eVT?WRyc7$cIni6M`hg2k$Pa5}ZY>no#17!C-|% z0-k;Pt}`qdj7wV1JZnV&U#}ZFRsEHdASdomu$g!83PUR}gz;PrjbDSKU9wCww;ep^ zj~8Wtsn?xE*yx^=9;!Ubpl%ubcc_yMtgHcKiK~L~9~uQTh7VKkCy{(9uBK|5zf>V~ z2*ox7$9-0?vSD`w*1xBi>}FAo1xYvR&XhUmISY_8-CYp8D}^sSh2FgI{^GPnJUb!<{nOTy(0iZ)#rCY;+H`JYU<>l;lSM#&7(Eg6l;l6^}2|z6z5d9q}d6CwG&_ z+l#Br#TYzS3g@+w=J-zIxH8^@>I=|0RKY%>R|O6$EB!EmHSOK`AW!mQ&HOt?DTi+R zBs_;eMZL2I;nioOoKpJc&XBqE0*(bE?P?I4dMzx{*L?O`65AL4^>#}S&vR19V%Qy5 zsr)V`sO#+ER(y8U>OOX7slJ(rib;ur7sgY%tOo)Vp|j6NG7OJDQc=(jo^(+)aX^u~k!yL=7&U^A=1Sb_7jZ|ng7f{+RXEp(CNnyzZbP2U=s8g) z+$u{efG`(0oE~>CmI=^H>SG#)GwEVS*U*y+5!Ky5)59kW)|0SPBvUNBQQkwe(&xWitYBBIS^b07@gud1z97M}3~EN1OCDCHGwWvvJhnKk;r)R z0T}dbRr$nAX>~OU3Hm|3-!kfjsQI51$Sw)lCcVzI=8L~#!4c&{NC%REU(nUC=9lt@Qe^8F=Mj2W*{uDvl zj@;9v_rlzUKc*GE-6ZQKCDm2A^+x8Ev$JY%tVSi39%-6v3b#zA0?}BihxW`b<&54X zV{>-*v2yURa5mSs@Od1wvaxX1x98z>ROk143-(c*Mslu*RnPrVL07(WBQ)xuwds)Z zXfPyaXJq5^6jl~C^j1a)qB)HkMLbellgJ`Gz-pMx5R)MsNJ0>ko_wmKFq4g?r2>~u zc39@(wAL7zHg=S*PkUx5EcgfN#dwp&7~3j%116#Ly+qOlf4^gFqyEuhwU*Jby@P(Z zl%>pkezxwwXL;|^tk3TGzAoL$_?+C=q;YvtU}#C$)#--1>t|<}-L92)4KfJzWTR6l zUVAa;a3qb8$UW0}1hz}rAf1(O(HO24$eeORr5?-c(M4Avo2HRY)yfcMdjo$M*4vyQ zb!Q`&m)pD@R+pYsI>>-M^24h{be&F}v@2)A`aA36faQ9%lIePrJqV;BSKY|j!cx2Z z&zCT^Y$%c?78Xg?s50v1TCA9(*u%PlSQui-sep<1%tx@_)B}@LlcuoX>L*(D5sw7j zHPZXW#oGLlA|q+|F(03St7b~RVhCe_P(|TgHor+Iy>(%tenY?%xG4>Q*~<@6Vvu|v za4+992A9xP;76G29CRf!{{eSp;sVQ3ZATw+8=^Xb(Hw{oJ|=x3M;|qNNvjmOb%g1G zJ56aV*!ja*V^?=eiQKb97pT5R^4WP@!H^;uS9-?s4^;TRZE9htX$m+(ZeJ% z_*4;@+P{6{3gdd49$YTurMltF!paB3ykU43I5ixhs?Ufyn$aBYYv!hnKo_pPlx_5B z5KxpvmnAghu|=^-kUFR-FP0OfXR>UAcHRjO+cP;nIxyOIWWlwyusGa>aW2tZd1i9R zUK3BaH#SCz=A-G#K}LQmXJd}v8fcnN4}%yH;R1vb zHGEEmee)pe6{_Cc3{C9^Xg1?hW+S=+V>tFlF*O^Ohm0cZ#76N;>Roy)v!zTl-;;1~ zk%DgpglRdXpZ?TiV|TXa1XzzSvv}(qUm!Fb+u#Bip_{%aJ7w$YU7idRwgP}$AD6?3 zSM%1IX6?mz$2uf>T18;t?w@sKB2Voq!HiX8pAkpXPx0XjxWVD(7rsio&<(Ri_}}*S z?k^y1rlN@z=?ZENjKTK<@)ijMxr2XX7bSGN=!p~g6XTK4p|AX*gy%_)RU$-XgoDq{D&edOtM`1#ah zPHtb$2z5kNVRQFN3`U#t(ar;IH`RzNkWE5F7GHWsaHYQ%bqyKUiMw$D|6Ods{>lYhrVQ6hvI3jaqrn%5w zAnsG&H52g-7NYCcK=PgSLLH178pM`8t?Qf2Osue+_7E@!rxk8S zAzSVawk`yM{4I<(4zO}JJJObjL5V-mjEi5vrmxV7pVi(QQTAA(V1`#l_3x*zRNheC z&-9<*9`qqGH$q^qX(NDjnMIwU#I)&g9B=Sco+s-E#IUhElGfxc)lPq`kbzwJ85HLmGYR(_vcH0So3HYqa38r!7u5QcYkt3;!oAd&QM-8j9uaKA z7w_vW;^DwrLqCJ!Rvj9Ei6KQtN0UsoH;XJxSlMsf`Yj>5X$hOHk7Z@g=C531z@$TP zORK)?D!%hYoQ)_#GJk7?99V;w-X77M<-~PZ#Zh#!f9k166YNSv&EGXBsz$0aYjpL^ z+(IKJl!+G{Qb5S_*)!^gO?o#h^X=35ml0Z&il(BbGSVlDI2%6JSQnF+ zW?@s1rUI=PaU%s15i%e#c#+N-ekMssu;bpS_z&C1Hw|4Z)3ZR^pHpm83n_HJBfXzR z%eG|*4wlA@>Yvsuy*)3RdYYDHKHuJBcz<+;+IpW16$X&wp3$8SI7?Bc-u4kj*}mrL zsmKs0bmZ+=gE&GSd7JeYqRO+=h}Dq|N#iO}iMv(8kGqw?Q>rEHC2t%QqgwK840kAW zk`BEiyzvuW?FfRT2RQpTuV`4gdwfpq&Gi!uJxCp(L^)=xc~d9OO$d=4tpulmLorFK zn+(rNnF>o9JNv&u3@~L{0#^6-hWmMrt>rekPtiS^xmaqqq%=Jy(gdp8Q#a+W24|v1 z*^rtW0S6ybal%Witcgg#TCZzxRITT&*bL9MpjbyBj?6GNq>HyqBCR2|E1n{=;gS_v zs^y^*7KMO8&Q}^13fya?pLYh28lJ2r`}II$($A}x><~!N)lCul8tHqGR+nH8Fq}GW z&by+EH6X51Z#s>!Yp886?EjQ^9v1eGj{hKxwy}&RPT)=A8B@2B7Ia?&j1nHCX-Jk* z!5K)QVShYDc&5kHKPB7uWc|QBE;#%_`YrdiZX5Q4p(oV0kXbT`JT-On-b?LHO={Zr z@DI%{QQ{&?DQ^u$1=fgpPFrLUzbeA3HUQGvmXCn&uP#y25b3NS@GpcE9JZ;EcksX3 zA55t)Hnch=o~j;Gls1W42)2RJN^Q0tzuJ^JGqD|;V>vnJuGYNPK5|eVBDoTeQ>X(` zBrz%z+b0BR4u{49QAd8xt5_NSNh@*`nwuM-jf}gGh@7*>h@7+UA5MEy6i}n&6=e$y zD!ZisNS&0T#z$QgWo?60L%IHktVIHHuuKCMl(Deejkv+%ZL74`U4qL{r{dw|jLBWqd_=(ISPa+|r4rV*cEnvn&Z41dC{lx_5rd0XXAh}QQU&gmD+)aH+@`xny&p}cjE28nLTL3@)+j! zfo;l}VLy02&^A5g?qx?+dH!Ta^MFQuJrRu!1G8u6eWMSyXPP5~#TDi}RClxgIeAc* z1pPLui>rQqY#Q1K%pNU|NlLAc&=3y4(#V5X0E_+z_No60QnRBPc_gl7(8%M2fP6rs z{{ZKjwkGI=xGL&l-5H*8!$7`h7f303O5D^KZU3-ms?}#n^$T~~ahXn%PM%7p&oybS z$?J!1$&-kV=l$PI6eeJFMB=`Iir4Rb;Qt}X{7dB~Xlr9)ZtCoy|KF=%RD!iEB0t>7 z*ZT2NAWwi_em=n^erE0tBLu86y)rbin3rI+T{7We^oBO`t)e*r{p~N@URdMIF3sG^ z^+8s~2FClGk4vrh_vvX}fTJ6-5Xsb0J(dWpNa!nj-jPWz*5@|&-bn$B2y-r@nI~)B zn+p}zTI~@1T6;4e2AC1Z$g0W566jxBZ{eq!&_$&sh8)%f;>;z~&s~gxK*4!iO832) zx@uM~F=%tT7yD)iG5K2yjO%rQ#KCS&&6BZe&d+7pwky$(&7KSOozEr}h+CIeX<63u z4X^4%h<*N-j0+gm%PeczZQFH`)7kD`R_?O1Lt-qEpx0 zLP=(=rJ;iJmmZ!=P#M=gN=-ZJpBOO6(6c(aHZ(QNXC0c8Z%0=ZQLN4|fxj7{Gkx$s zDQ}sPVwdIiiYKCif4~TDu|4MKCRKCj?unewtU=NJ_zVG12)zwM8hW|RqXpMR>L&7H ze*n_U%(ZMZhB>f8B0dX= z*hXjt)qs<4JOjF3CVknPZw%0gV`1Y1>REss_liH3y}dbw<3SuYUGcQ?pQmh~NA+^Y+;VUat~1>!z=hJ}812t|fL%&6Fw4k_vaLl%5P zaF}0KrvAe`GL@YpmT|#qECE!XTQ;nsjIkQ`z{$2-uKwZ@2%kzWw}ffj5=~v0Q(2V? zAO79<4!;m$do&EO4zVRU4p)ITMVaP!{G0(g;zAMXgTk{gJ=r826SDLO>2>v>ATV;q zS`5P4Re?-@C7y1y<2Hw%LDpk z6&-~5NU<3R7l-(;5UVYfO|%IN!F@3D;*`RvRZ)7G9*m5gAmlD5WOu}MUH`S>dfWJ! z{0&B@N*{cuMxXoxgB}fx{3zJ^< z9z}XHhNqMGvg?N2zH&FBf5?M)DPN#Sg;5Og|0wru-#o*8=I!LXqyz~9i6{|yJw)0_ zi{j3jT#nPCG)D52S+165KRchAq|514-eM$YPimg2%X+16RCArIZtlDbDJO9=_XyMD zoC^b@fUv711vit4&lIo~XncD2uCrfuKH8E``e;Wk&{8k);EWqCUZY4dFLKdmDl2_o zMP+GW-dzpwsUA(^%gsgRdYf#-3OCJUsgmJ`fGQap4~PuIKu)ZT(CxOSpRyUl=$|t1 z@@9CcP9_@rSKUF|;BN%KHC+N7d4VZ(4JNDI)}~sZv2!hs#<)>M(?2^H1`Nah~_taU^n*CbZH+v)kdrHiM?!|KO#%*anDcA zed#~O%=w^jdIN>J!b>@<2;X8ubcCH!LUaV3T0*)*P6lv1xM#U>JO~Lka?P=Kai~qs z)|hDVH@#0tM}OqE%ga*c8vmF(0X!4gj}tZqMuEekF6fS&$@If4oJH9PLW&Ca2CqS! zfkAWlfh!<(6MyR-lrwS$!W1cT&?~9N)lQb(4OtXPysW0aAuCFVGK)qU3A{G5JDcRR z0l*vGOmm7i3SwqTqa#ANOHJHqtXj*J-5DUpWe*|^!LSE7MH;VKN8ppjX3R8gSfnPR za?2F6Xxunau(+jZc-<7%)%3K*{j}AElzPIow3=~#ISC_ByScS)c5RK|nL(TH%;(lK z^u*J*<(dfJ;}Uiev!~7#lDhATnmpSY)w#;Y`=iAW#6`}@HGaXSeT;jsEvDL&Rwu?g zwa+JW;0MPS06x|r$VLq6$(ka8!;gGb1K<%MqGP+vDZWZJpLjKUgN0dK?p3C{D&tcv z?8!@{Tp?UxYWG0JfVo|U^rKmRPEB&^qgnQp(hU_Mp`Hw%ZX8fw*h*4tt04)@@mcJ_ zE;fJG*eg~9`F2+PL4%?p8fN*l|`>hNJhPR@f<$JH}SDGe|xPodBc@ z>*Gnzv5JtD8GN(Z%CmDFt?t%9F3^cpug_(Pj_XoBpS6RydL6+wWw4E%2-C%D)4a@G z7Mm4d{CY9S+M^0d1mLZT+oHVm5%c>in{0}!k>iT1C7#O+0_1Gclk$8$rnAyl`57^B zo9|71ttYuJ?CCDp$oK~e9lPh*aS!gBLQ1$o0w|uluKHCle;NYURgv7Cg;E*M8+;83~Kx>BJqZ=o*mJS9Hxp=bp~uQ+Q%iUB!>h> zOs3rb^x>b}>%7ncd=$S7FEv%w)~kN!oh)w>XYRbU2#{7MtEP=KR`!!n z@c6cm$`qZ86iAb-P2zW?ffg_?Xz?EWLv+Pnv)j_^g>gIsDw>%z=48xXs ztXy*AgZ}XryXSSAq;ZyAo)P&1<{h#o+VX1pS&x;c*LB2ys@g^|Ne^e&u(F($VQFzr2N;Uxpn0XHISA zuG$StIAZ#%^;gdx$;F0uJ&fE3FfcOV5yV(?_06FH)#7uOG>hC+zoVY1>30J3Ep>V)`nJL7 zk-AP2lh7;4f1R`YHyo;x@iS6P1L=R_8g$rKjBniGG z7Wy?lA+#98cwsLqlOX_;2mj}QgJ00aae3PBZO))?g054Gt?|`89P}ud8M2P~c zY2m?A{f&}{PvB%59$#`Yk6F9}LtTVLr4`_vUk1t5EDB5ygR+ri}TnuVxHj)IP*)IkApp`A~+v|BqN+W)Eh{|~%!crx)V;Kr^+pMkH z-VRyWpnOF)zmUX=sW=EW7Sdz15#ID+-r^V11Ir+;p$0yW;Ox4TAr-xrzn_b`k?bky zeItAr-#I&+|GRSkvlRau-}`?TWtEDiE56bAOSC zXcKZ(B?@}6N2NN5qNO?(71~?1N_iSEI}#5>GtgSGfksdS;%*IxVesnmc|!B7!#As( zgkcT^N*WT)relVUBm%nwL7Ks$StYuLd{O9NFq1)*nGAwTTHGTa$A)1vhix>~^ zwI|7g-%^M18t{Wp1E^%KnR)wZ~8RVWvNJrwz|vlMs7BF=)# z!#!W^ejQa>_i{U|rv{Nps!~_x?0z#}RB!+F_*)hdG!fagq+6O|;|V>DK|}OwLHM{7 zc|Q4JDqZH(nqF#j77OTDd%tU=1^eF_*XUDD zLzIL8?i~Il6q-m+m~@v*S2Gf6MH<43mrr3PsXp3Gc@CI9CsQ(oIsNyL`y-30TZ)y2 zYC@-4t+WFJjTIFKG{Ik_q1EU8u@@uFmb&W$L!V4#wKElaN{V~n%%E8S=L#i)yK!!&}msL1A@L^Cvs!?xT_*E3Wy+?&!bM>&BX0zj}N zWsjWwc*VWfRRw=egZ{i2*C%@Q6@@{UL*b;Ww9X^`b!$qP0Sy zC~!r#ku$&SkWCvn zA%wXT{U&rse)rLT(?kEqV~XFw)Y(gt1=pD3_FfE4BEggPx@1S6tDZ0ZScD8*)IFipTitfM{x-f+_9Ia~$WY){ z?tP3Z{DseC&$!T-VRNexl=}yi$sykaFt&Eqqf_>L$NZHPzs|)+crni^~2>p+%^0$d5N?uxWfDg`lerb52rkr$|fC*BhMw(nq9tjW< zVyoq}-AbIbelzit1@;rbH?dVZ4>&;pH95<@;rcru?D+W{vzL1c+X*`pA(KcEsv0J5 z8>+;r?@uE6ZVy`ZD%&AHgeSJFy8&PgBs@pVc#tnfT3K5lV*sXjUg{__>Bb@itc03T zqY?ocs6Ce36GFD9e(^6_ri{W3S%uRcdhX){d6o=%W{9G-wuW=;LYD68tlaYm5QL(>p!s%^L(DaS;O>oUeRK;kuUa~kLY$|&( zd(+mnhx-oK_v;PQFXh%6i<6GnkRzH!%2|(d>!cUjnvoBDg#=J!3L2v*2pgtSQ*Gu z=RCC%>XTs;O!aDy!=X%QiK8w96-@&t*Yed=2*U&LS z0^$6&T~hZC?1Fp>6%{d~fV|qvj(ms2(Ua!9Dg4-@-?flR%5sI9p(hOK^Qdv5}Xb=$>(jo4>I*u7NUC zyw$-D1RDY8JH4QF@IEYTf;JSon$LXTqQLj_Eo^HoZr>5s!0W2;3#ol30_UhcLoGP$ zkgJGZqf;mXnmRac=Q{0!EA1#l)h_iV6jGE9xOGkji}=nk5xH7<(w?_Ql{_mq#X^Ps zDrl19$7P*mtYZXO;`>IfGU<6IfHEoJLRWA?c7mlA2snEJa+2G{F|z9-5Lc$X_M_6I zS7rTj8iq>V>2qDS!$9X$3AkeoqYUrRvZZlu5AXhe&-qj7DINRpJ=$nbm&yJUL zcJ@H|>CqgW{xwFY`cv)wN}Xp%GW9wd!vU)01INOK@s$_sz16F3W2^K@64nUUezH@@ zQJiU(N4T!2=C0~dhUNu;Y&_yVmEn~^nk$dh5N)a%9~XmIbR7Nc8u%miPwioLEmHR* zySN?!T9C0CcZeao2$y3m!0*@y+9t(59hZ=ALbQ%d^GQ)E#qI^ctA?{nKcx$+W2A#j zcLQb5NUIbd)gvB~QWr^1ng{>h?Ow+v4w|%dqIcC-N&%ap_Fz6b`6n}Ti zlkcCu9o78psV=AQ@NEwJpC&!OBKiLjt|$Cu)}#UDa@ZbfDL5^M1T5T#IOtMJZ4M~@ zXh*~47lNRu)o#ag&x>oab^hT7_!}++Tu>Kp?ES&$NgZ=ft z@|%3a9wO!rj!ufs27i70Pfq5L%DKY49NedjCV1fw36Mcf1LIukMiBT~H*#ef1u`|^ zS>3!r3^IrW&|73LfNdaCC%H8HKgW?VdxC6N;*dy^8U1woISrmJ&t9gk4IS(~pI+}j z@q&fnCqtR$5RhjBLdEL&X@l(~du#pHwHPS`dQ<&40f&X%>}7*O-vM#J#po6?Y!?LZ z#%8kSqO^!ie^^+#kQpbo(yAwf6w+F9{5 zxr2E+g=yfXY^^*w^#T)dy*>{ssx02%=D=Iv@JdTqIii;(pCh3`y+{r`Qlv~G#KJ6+ zr-QLYiWxU8f%SEPjUe~u6gi2Y>}jl6O(nUyc^qx33sm-56?`f42*06OBLegREfmbNUvvR#>{W&4DL|NPV+As&($WF)rTOnFv3La3jr4-Hn6zUC4{4}gS4p|j| zXte{N$&J}b9RjH;Wk(fQ8MEm5MeheCL`nuU`LK6JG^(7x%thc4+P}<4YJm2`*J22c zv@7LA`$kj)8W9K8B&?Wg?{7p1U09yEf`82HVE-#!;om=j{^PFv=Zxw2&%3cI$y#>) zTgCC!f_Z)dib)na4Hdu#m6(?wN-ysPJ}QLh6xK=aYKgsA&Fm_COZcMgg&!u7ANCJQ z1XoK%L48~Ry|l+P`}4*&`|+0JdQMOG2Y}pgI4JTwMt$ljskkbA1%8w}3<-)-qB0f3 z!I@9PD0ju48_R&(5GqUqe(T|y$)@uJsaB(vrSrDwFMP-G+sqx7fdi-dcc~=&t}{(w zTCssQmj;uFlFp-e(*|_9ORZHD~t<;{*$w zNUR8S5`2=qbMkY8gr1sJ%pa)y>%Zw3wB3ic9p(>p1~$Nh_L)^oSkM);n2a2>6QF^* zQ3Xp|`{@>v*X7L_axqvuV?75YX!0YdpSNS~reC+(uRqF2o>f6zJr|R)XmP}cltJk# zzZLEYqldM~iCG}86pT_>#t?zcyS5SSAH8u^^lOKVv=I}8A)Q{@;{~|s;l#m*LT`-M zO~*a=9+_J!`icz0&d98HYQxgOZHA9{0~hwqIr_IRoBXV7?yBg;?J^Iw_Y}mh^j;^6 z=U;jHdsQzrr{AWZm=o0JpE7uENgeA?__+QQ5)VTY0?l8w7v%A8xxaY`#{tY?#TCsa zPOV_WZM^s`Qj|afA8>@iRhDK(&Sp}70j`RyUyQ$kuX_#J_V>n2b8p4{#gt6qsS?m=-0u0 zD_Y*Q2(x9pg_p3%c8P^UFocmhWpeovzNNK;JPHra?NwY%WX^09ckLz+dUvRC>Zu(= zE0Rq{;x~uY#ED&tU6>T)#7Tw%8ai&-9Amoh5O$^)1VfT3Kefm=*Pq?2=Wn~J;4I3~ z*>@-M`i4Ha{(pDXzdDhCv5Bq2ceu#EZAI3Kh^k0FHuZM)4Q666NzE%_fqXjP{1tp~ zQ1Gz`Vb+N(D=pG$^NU8yt5)T{dAxaF{ZoyB$z@NPrf)@G1-$w5j;@B_B(;6^#kyDH zZPVPxZPVGFPoIz1wzL3+_PWFB6IuBtIwEL}Sm@{oD8^Jf8UT{5Q@3HMRF0M4D=_E` zD(p+3wNv(r!=OA#^r6zxnUQeKY+Tj~-6J`c$SGNlHTst`!>PT8oP64JwLJ zo0&FdEy@+u>gWQrXTdhK^p&z61G=JYN1H5KCKeg|W9c0j1L*oI77G&T&Z5-HqX=VZ z#!c;28ttj9QSrIsa5}SB8OhDXn$8_FWX#?SWSGHu>Z|1%HI~2`_eAKIXQ46}WVn1C zq4Vx2!Tj@NE9J(=xU22vc3x9-2hp2qjb;foS)&_3k6_Ho%25*KdYbL>qfQ#don@{s zBtLx?%fU}M{>-*8VsnKZ{M-OZKZ2E3>;ko6$FWGD*p9T!CSb=4~c)rOoo5E`K0Ic^_ULF141!8WqUJpg$IH=MuWY`+G@#?Hu#}$j zDKKwbn1(V+u}fexB}_7WjyMn97x-r)1;@-dW1ka*LV~~`ZMXb5jwOa|#_kzpH|1;~ ziM0Z(3(i51hF699k}j_R#YEPp?^MUV~lprsYT9X z&C;nR9aPs;069~kp*WuEUfXSpQ>RR&>8I-|<=)3VsPW4F^3DhBOV6Nm<{%}(LoVbz zXCz2qe&_se*qqX*hi8u%6IS!95}mLi-(R#SvKM_{jFaAOIcxIBVb0D z#mxPNiCzQf@=e5;1EQ@f4{xlXGooG1uw`hnwcHQZLq7i3=x>PAecmrXKu~j`52SO| zuM4u^mx46I<`|*yI_~W;eFi6u51dm-AEW(@z|V9K4!C*wD{)wHI{4e}Yx$lynI|S; zXE2fV%8_->;1VDQXej!4Ogi*7WK5aj-uw@PdJ{y%P__4KNhoh}7HN zTe+&l792&XU2;`=>;_P>=;%@BAP49r&lpXeMrS1>Y4#0|J+jcu^7t0z?)9^Ups(Gfh^lT~da7_I!7SQqo`ayuRhc*HoBNP@sr{-|^8? zZO2pGuK$RS-u}UK!vzE+%OG}2?9bhm2&3fGYLRQRQ|9j-Y$VA}!DbMeL`e#L+sv5= zjj4V3+jU-C*JC8#R*`7i8LXcNK6~z+3=NitB4?Lh^QC_OW$sovcgmRdCXvymBY|-@ ztoIRZB6?q}#u{onCGn>H+{4iFA}o)(%D;-LUnYogL75kPIz`7E<~wT?Er_#ySf|aC zV(OPMl&RHZ+~lEHks$k(dahPU-n%*=RWxi_LmoyHn%Xhs`}=1Z7VzX@sL658PZ~r~ z)3-wXUIRX{mgZLx#p(P9TE1W>*(hvysV0P~9&Kj~vh_DYUCXw2!u+v^jWX6)+e922 z{j!a28HTt%W<)TvR5oDpvGZ2HbW+w{5yIjn=VP345an~xUsRw6M+E0>Yj z%L(l~15e>#g<$DAx#;2NC*lZ!Jgj5+uyjAGo%6HAIU}fGaKp}2Z)gwfjLfCa@MQNm zUXQT+U=H$fAjHv#W5BUVGinxT;W*b`BL}CX-fvd}$ZO!aei6wM4lvTSq1US%r@>b| zHOqrj9@-~x$+*(lL$$zA$oA?3M4-C&!c#q~H_=hl2;2n*%pNDN!M=<)zCx^9IzRus{1_>%iAM{3Q?s zIu~?m^B-?+TrwsWeuO-)?BonmXlc;AmRzV&e%-Hz{5S3_UfzCZXlx032W zT&r`5@e2?Q5v0)Z)gs03?%Z{(bg*=^ie<&oU=0QO;nA0ON})kq=^uX4b*uT)?v6`2 zwMgyt^sjpoc_|NjcyUL18e0u`Gj#jg-i@{xeM{f;`>%s*lDfN-MdsW+>!Zi)m`c6hL;eALmV6u+0aZrzWGeL zICYR@_=fPc)$s3}jn}?$32DP;h@$A-Dh)QEg%wTMGpnZ9g|~Vmf}-KiC~PcId9XNZ zNfy2&CwYf7*;g?iVuUU64A`Gq4f)XA$s!mbc;a*a8f(A3e`wySVO-;*M7dXh*>sRtw$iRxXe?7VPx z)^wzvs)QWJUcB_?N2d^{Z9KKssXr9v`3(mV1I4$q{RMlfp4q-Bxf@St-Pw3Q*Ef!$ z!{NR<=B)=|K&A(zG8TQxik5kFerKk^W(N6`tJ(+C8ka{3yfhI~zuw$buwnXgvJB~x zC)%fCrD})mLbehXLw+LA62K1)!9-)D$dTZJ8+OY7(gHj(3BjTIp;EQ9$l+|UF^9d_ zsI|CwwV*tyG>^V5@L|uh|BTI1`Tte+6;OF3Y1agIcZcBa?(T%(8r)2gLm1+c^ zF&AsjmD{avGrXl=zeaw_TKjkqz`rgOdHi^$aKiNYam-b;L$rme%A2UCA};#Re$5-LOr|W;3ud&1mY7pTQ#SM`Vt5Cd zYEBoHXs+%h(5*H!JlUzQkBSOQ6Lk`ou!6@mc-nlq%Y${&A$i4?KLoNyyM5fPPJlIG z$G9~@>H1RG-a*Ro`uS9+1cbU)O~xfHts+ENu#IO2urHiJn7 zdbB5vQBTpglO%Jn1jYCPvBhc_(>gL#ci(@;{t_0 z*^kA(phCrIXxmdUdiv};x}upJK!~nM@Tr%%y0SG*e6pAwGTuAJc~XAGUc@zzkfKvD z=sLfn^M2BD-OKDK>-N~)U%KjZO-u;d$4`MK7+kj`hxj6tdcFmwo2XJb`r!E!oPoh6 zZ`gv)Os7!uOwUm}C%%uPs)&?AQD>f#tcdK#N11l>yi(^0r8>oA3yp1*N)sw~#f{j2 zx9ZQN5jy-(4TV^S+$^_5RhSA{Gwm$2NYhh|rKKGx$MUL!(`{ zGHkktj35?Uc-7&ovz}hFGC2bAvyV`8HfcR6$gt5_b%o3g^ZodU@`A|nv6qF_>S=;>pi4y^+co?x!?qait97Ph0;&YL(~eiz_LI zZgkKuIc=oA>rvjEHK%g&gv)lP!G3vbNsu$)giUbeI~F9DXX9J;*n~6 zilm|`+CJlNTxoHKlUk_Y@cq%}R^OaHD|Xf6HhW+A)b+r-_krCakhjl`J~x9C)MPjUhAS*_Xx&3;)AU->jXas zQ1$dhp9i!%PGH`sFM%sSm59?qR?F6q!0Xpm7dLS!X|sXY84D&6oYo$l?lGwsuOZGf zoMfrJ(SzO)45AzlfL62(QPqQu(4F3g2rgV_;QLOh&RM^XSrHWeaq1d{=n3Zl)5r_T zx{#i#OHF>Y((l=fJGf8i>}A5=TOlaCBEWq18juk4Ti?X8W)2R38SEc(1i(uBpC$2tOJQuoLTL_=fTQtnoGAIm~IQ%Z%LlwcxC zIU>S{)58khv)}|LEyCExa4{d7X}dUa2$%SoE8t21a#@GDX>%98OY3V*X-~-rb%D+j zW5h;^a;cpHC63VbBUz+C0_9*R5$rX3mry-{fORZUjj=LCrl474o9g21VWH<@UhH~J zUDt@LHO^ZE_}51;0h4%ch8&SZC46V~+HfTr2N|N`@>HbCn>Gzjla_Nn;tDm}i4SP> zDOyTlI2E$iLrt{73=O4BUtZH=#Uz*tS~>|h`d-aF29nOw+F?>nFs;tWHMt;6VG|mC zAdCH+BUESNkgA|%0@QWh7QC`txJ4_rI}+X!k4YA}*_man=5ZD2Jr)(lOBtj=z2>qX}Gvx+3nNSd%coGbmrl!6@g%xYi9--d;<>eT!UbT zM&T0n6K%yrJ>wc)`ta{{`jrJRiA2jISY~&=Jw&QzYH|T2U__imTv)`1d}c)MWM>CJ z*`Dj)xKE*w=r!5HY0GS8d0;*d3XgBBFCN@kW0-mFJhLhIBUb9l>ICMAgTb#aThHL`*qIO_6R3STm?k%hlNH9KkrA=o^1S%YhbwBSwWb1&RBJJD44Hp+DQ)7gg<0j%>3|V(LAuKB6}9-^Ii`#nd=vz(21+P^Z`Xnv?-7NX(*m|@2ll?QF?|24{l-j1kT$+7ZcDf`yM|>4LV-Y zoHwQ)gH3C8`l*qKrP!=wjsvg`sENhA2`1zPX^RrhXb>Vs2z!hfg{ont^waepLpjVFoy8I%b%d`K^XW<`R{$( z%Y^a2#eQpSW_F9s?I)ahzyrIB`W!@fLeu;D%xI(5tErs<>3H0qeo`9;{2IFm*Ni;H z++0Pw+&xlnCd@B_P_c<^bEP9%=R7nW+Z%d)vD2w!yq6XXN(uhPv09xP)oq6C7}#*P z0^3RYJcTDW{u!@s53As=5*FuFeSyTh$(%hsrOn`^zTY`8b7vZwX;<|GTobt-OXYYT zSukNSl^Vxt?LiTFg^FSw&uM!^h|#hqn8vI^ryd}=0yoAn!ENxdhY&SoeITRdIP^Un^qPcuL~zsqLYN&WuQYP4YP`HRUa6~k#ncCSuhxW&)No9T!SN1cmidtmZz+B~d9 zcmg~ZLTAH3F&HVv(?L5vHsdmcC^TI_ete8ATZ58ZI_J#L5BDtG)!!ufkZi@Z{OQDz zU_huvLVipu$0Q+xnW8XS6&Jy`T-Qtnk@GxXGA0oA1~aJJq83_jRLya;=2ezx5=kfS zSCszy8?)h?eH#36E^mkzni zv@~A1RQFbhoj8NUpQ3L>Ekv-$jUHK$JqDnKaNYF%D_?H&Aa^uZmuDHp^fxisktTTF zIIM3g7sblgWYS;IE)7IA0@~gLujedyZmqf@+M$8d=H^51D^ib!bZVV$DaQ|cO5iAL zE0TntRCNIbFMl^Y>}p;PWG-V=dJ2(^&^Q%v>B+B^rfl29oriKr5}XwdS~tB@We zyft}3>ELdiuoE!{W5x9-WQm(d-8<oH@dv z(XOT5=w&oH4Wc)hh~5?0Ux!EW3FCtp!0?y`i19N2{_yxmX!nm;S-c`(9ss!SQwa5O zb=rf86e<473$#X*8|3eMu~A;XLV*Q;hC5VCuyjdX?%B<~0ey-WA)M%jILQuoDWDcw z1if+CS>bj#;@uhjmx1rAqK&VF6cI|_N|Q+;`Fccfa7Jb&lQadksu|jo zw(q(?f;p~JVVmlBpQ1J4T~cR_cNeW4lJ(jxLh5&7KIwmUZX9ILJm8&6rL5u0Hc`w~LnJB|qQSIGsC@8$&;*E8qb!tTP+r5^(egeo^JT>F87 z=P)MS_%cgUL=UF?S(MGtv7j+J7-3J{wLt~5n`;G5G)`l_-$TpVxZitgx72Qr_jyna zOTUlQ!{oBZjQ$oKNdv2|Q*7#5W`IQvi?W=bdAukme8ZKYtK0`C!&ns>xt}z75|?dH z`5m?wusf04BL|;pIE|m#eMpqkIyV(>4_@dfq=TD#$p~}bp(+3RV$kg~?GpIJp-UDy znpr{dx3SG&p2tSm$5~9-xL!#eIl1K7yupa^RGfU%)Etg-^>a+SPoD1sIb9%~v$71J z6SA&313{-4`->=sbkojm;bBo4!71eH^#)@j^tYz0M6CFVFx42pypEQ}*IR=r@D^I+ z6O#%Kh%>3hC3-}QeP#*D`G{9b6r0^FDEjV2u6RqCEYlAigaAMn|Nofu{(AKyU1WD+PxIuG`w(cYcqHzG}H?tD^`aH=|Ol72C{GEaivVl zqE?l+#$2$NI|uvpdPvnngfpHfe-0BdVgLTNC#E@Y=@|$s_{yjYNl@`a%_V)wh`_=( z0@8-&p!tHgy={vZ=Cd%x)~w4j){=Nqgnov=^3@dv0qbt>B8}EHKS$bdw!v-{&+y_; zc_Ef;=7N$%8B1vIj8xfmnKi#Hqebs>rLV)pzxo1|g1Kmq-Jy3AY>I7sm)^m|D-Y z(mwkH?~eG9j$!`$?Ip97ZlQV%g^D?+2bZ@b4f#kw zf9RDy$x))hIJ7S*v7oTyGO&I3WX6;wNlA*bf-*+%_$<)X9H;59NPEYByAejlcLQ+) zQ2Vv|Y8U`&HkmcI9j+6MlDWwnQGTpbr$;?D4c}9_~^5_V{Zan5~y@^OpO0cFt zfKm5X_CaC0(v*eqa68B~%@&>4EZ{vlTJ?>*+c_d6Zc+0-3oP#$S*A#^4Di4VGqo+C zw>3iYS6}}^4f3eQZ~^HG|985TvbDuOtgf{x554^{W8fVi(PI*TL_7_8PceW9xt>k@o;W{W z8C4^wMt6=xadX!&I7KGz-N`#G3AB$?7~a0mYrS)w5jgD4RV}>9&`3h0cI)3(jux-e z+PscNwjPfgj)Au9GzDQdp}80)1WVHxVmr5bI^z7zhDGdHLoHzKziwvW_uvXYAh`@R ze?m8f=QKRTP}pE3KMS(4)s>7OQheE;@7{ zow783vfzmCTWSKG&K7E8-Ke(WL4fYOhk1+yi2T7${rRMK<92f7V`3U@oGo%v#;Vco zikFU#eT0-`1Mge*^siY}28zM^<=+=qVxl~l(#;y=Lk$QfSIn}k7sDBKHecHhuGBa( z>a%0EpM2+Msd95(v8k!$rIgW0qSPzE64Q7!H8h(RsyTj~oby<0z=^UELFGO{KHL&& zO`>!`SS%!%7^(*=R-Ls$hMwEl!*D$|)xj`>x(GBqMVMeEZg{uOh7qdAvTV*AoC5bs zn4BK{*xG~c-HuAo)ni0&-K&{uYDN))PWU^u;3!nMY~{DL{nyi^XGjt`UM$s*wbT=I zGHve+Afjrkc>`+?@H-c7@VD@|qN0dX{biV+keKd4gS^Dp@BR6%&6=dG$nAp)Za!KnLO0LhS8wj||^M|61DQ&zKKi0P@^;+h$JU+Z!(OmSf@mO@X(A@iSm7F{*15b9f zxp~0rQQ11Xuzz}7=d$u}(}W_z5+?s`68_HT_Cb`7U|^>b`;j*BNseHDD_fE4P*iy_o)k;TqsOvp< z)EGQx)ycApkQ;K%maA)#c#m$;B2swX11;^;{Kgplya{{myJIzjR5UZ?)K zR3+8U6iug}g-W0La~b!5ffBY;>sP3hrCwdl`EVD}>aOkNPhkg{M9fPnM$BrBOvqNH z=3yzcM5QI1sAWqt>gXE0lg! zYV$>we+W>Cvylog^ZNyxwmPu3Jhr#aF_g@xFZPSPUH-%na{ibj`a zgQ>%A=Bn42s1t%zpng4KY?_V*H+bBg#l3W=0!eu9&`DD)_(|Ac~MZGwip`reT6VppQJCzLFk^9xf^u|4BWPA`aKAvB zFy42LenUHNdrX$s%yGf?8Hi*_=0d*P64Wk%jNs!$Rzja|PA4jVKykWP0YiS1gNE*#xeqWW&^@ChwUHkI3fZ0aH= zTQCb1OkngV8H9RgXwV_}ll_~C7{h7-wJlQb;kP9;n0#G=JwY}|yN5zTHsOwCB)VAB1)lBz)Bi8|SbdM`#?%>S{qD zaDT&h4TK26=9-OV(MM*t!M#fX<6_pbxMtlpnH93fSg?^D86bEN-HZLs=mdX-8kYZc zW@&Os2+tMnzJ|J2MDIOpTSDcO>z9tq?R~f>+*P2op-9>Qn=O#{0ina5Nr!ZsxqYY(9f>!R7-Hd&RY@JnosgJb>&m&9?@-5qt&|djw;sxjL{Aljky;cd?Uv{${i9^U?m zaba!e20Y;Y)(tYbqU8b@m{LWH1_qXo$c&>GD}vQqVfTw4F=@FN$U=QG5$b1cJdQHO zAP#eSkNqCzf<9wZ2!kGDNXYR??InL=Ld!a(P;~ka$zRM)8c1-=zYp7TAk(vHYJ|Qv zvyd%=BNtDxN>N)1?+tSS+dS+6EIcV;JHjlNoDPA1>8vz=NHOfbl)(uZsOCV1cfw#7 z@3dpa49-bEG{#)~0G=9_y0~6#9H<6+N?D`y$w+LQ9K7t%GvXcl4V;3Z&_F@H2b+_Q zG7j)+D~@7oC~fMH6r6V~Qj216IH?x=C|-v)-N$Kt0-C>>WTOxAR$`$X#6X*Ft~FOV+9vZ8gEY4bpgpXyRODSa=I1`M1eNZx z%-;5XOg`+Q2n=qM0{9PQF0<8a(GH@7jlzIp7wMFQ_(nar#g-7}UBOm7Jn9_ETi7lK z0;4?HWSj9iX(1=NV)RllS~CeA?re+5j@@8)SYE^wy@q5=ighQbYT?Z;${NI8j)8D- zNPvGhZUcd?m@#pd)%batngdIc=iz=FdH5|n8&*Ml%VN2;QARXK>}7#k-|JUUUv8!? zLUT?9AGq!#dcrF3_^zl2+F=CUAz@$b@6l7Dq%nCA&hwOKS76p+9~#Qy=5>WHci*@B z%ja7dzv0e#tyNL_oyj;vMdLK{)FlYD0-zV+<*R^4)ho7Eie_EUWNAoW*?>XoxTcl4 z#ZDEG`Y-~SG^yX?uTVWyoP@r{MZ+h-)uNaJ#)e+_0J+Np~bY&M&qmeU=a z7a{(fyQ-T}@)FmD31Pwm^lp8&{fnqIo_K=X_B#avUWn9daz-%)-puH7w(eHnCn$GD zKQ0GGVyw#eCZYmMAV|x#W*jM8DG)kZFfE95^(|O^J9@RHopybKm`Bo7u+P~|!PT<< zIIXo=Z7tawB6W+(hm>ciO$0(x=x)Yyt{?+TB?BGK7~jV49`U=6>4Cj!-xp_hf}>z- zMz_cG_%rz7YuIm1-y4B2a)Qm05hf$lCxd<-7SV*GFY_aq?Ha>bW1nzAd0>UI)AbM) zC-Ffk)oMROzC$-%QWBV2&m^zvV$E_**6Enn7pS-y&gUShc0g$@shSGu_kmj(lXM%z zcN4}smA$LCLYkvG0ZBR2e5|bnIJAy}dCmf>B(lB&+jrk<2@XYVyPB|1%E=rJ*`q76 zQ>Cz&2I@k-;-G8|6l`Hjg$Wvgu2PVAAO3}6U`wAt{loCAC0*0g5@@2+q_jXS9pT$U zZ{LC&FFDL9@k?FjKr#dwc}yO!?+RZktJXQn{ZWgmH0Il++(NJQCZXqa+YiSs_q`lH zNbi~u6K-Xh#_@(Z>p+3$FO%$%)=q`;_{-1qnOW6AtP@Go;kD#3*BPd(Wq+J4-IlsZ z_Ujy4qJN_p3v4>w9KlaT4xK3U`K!d*3?vMS8ID-Wkx+_M!ZJd+9O1 z8FKZ)2A~hC>z?kZJL4|5T6g$!6W2`@C7ChJ16Yeh0;3J7L)dv9Ka$ zeQ*GFS1iRWnZjvm#p{zWdAJkqG>jS_O!fed$Mt> zhvcor7OmE&nzE1e1sG&3yb4Lvu4MQHOF*L!-!5Nq9%$t40M|S~X4HYV=f9m)x$q++ zb|A?^AFq6c{|ukyMj&vWNdfi@6TfI<%ITHWQVqHQ`o{W^`Vr^qn>hcgI}R$J2&X*v zp?R#eawl5axqGz)=xlec-D7pg?{IbP>T0QuDl3@k4F3=8lo|AgrsGvT9fioB1X z6<^NKt_=n8lbi>G3vIYP3^VU22jjfx*k+NW(7kCKruYnFY5**v>6oBTrN1-AfoHzI zGdp}B5>>@A2V7x()}sjYf;>sqBl?Z1Ju&%eh|KMA_numi3eRhFXZ{Rj6V<}>F#J0L zz4txO(qCNE4hHEa(=nbq6zoS&NS+BEC-fK*gqaR53#ZGAG)533_ye3T-k_=-@d;Va zF)OKHa?72XP+ZiO5P^pVns4>I0xgUMJdxG12Uz2c zU*Ops@xMr5*L1Z`iUS0B`;dMas`^)a%JAQL>OZ9W{=EXM`qc+V741oEv42veTDdQB znM`?#Bf*F>m}K0wIBb(l7I%@-PSREb8pVQGKFTC+qW^=YlyvrZP<}pGN`e=2m|q?X z6d8X4`_1Vnj!nynDqBO^UCK+W%(d&kqHX_EixzKNVsR!5C1&+AQftW4=FG%?`CQ!V67fM(zr`6+N4 zVqX|jThdxJJ?VI4qM;|JpEahPr)bN&$W63UZ)MXKGC6P750&dRYTnvFp2EHRb}i8Y z2yMyTsjkwyp3*#RwD>jyH<5vd#$N%(>-L6jv-OU-{prPF-($Gc>MkeOYBlhq$C*ko z;ASc`QUYTm-ia?8NwS6p(6#r+nV~_1C1_BX1)P(5o!HHkmi4R+tWmb zBXKI%*M=I;AF1R|+>{wBZg=#i0|mfG<@@YQ^Nk}&eAF`4a(LIp=)qT4G6`;d`rUY- zWirq2gT@h8SA@YNjx3W^S``X*90EQUM@JuO!$FM}ugy+MwXRtAV2 zMRevB!{h|S5;!ucoJJX`7C|1BIyae1@0xSj=9MHur%05SsYdRp1ZhliMt{vZ0j73z z9BTG^s;4Q6*8n)bvsqkBq|SoPlr~LS7e*7}wFpeJY>r33ZDN0tH7P2=bBoGncjc_ZmXA4)GHOELF6Y z02d=q)sE{EFEIXg1X9DJZ|uzVtSYDvko*a!C~VzgM@A1y9b+dBP!O$l9BF&STg(v{ z(VypBH9xe1w=3?@*F>QWuf_SJJ@~7Rktqhai1OvGCElQgh*kPi0xUs*ZXa1yj2^@~ z$QcXQ8gB5!-Il|33sGm@j+ExAeq;R@l3pR7ypkbh)5~xuBlx*}7v^5Hm#)9DV2w)! zttF%o^#Q+hWQ&cFoG1;=DcU+iA+4*~rLI~DJEh=gP@4VNUd{e)I7$;Dy&R7*O>~pN z*cOW2c??GiieaVA`As)DnswJ~1|2E| zTWJD>6IF9^9})2=bUXy80Ii@CqKdD(Pj`fKA#7zoPKZcu`ND|?yL?%E84QnFVm^aW zy5M9tX+o|F1nS4>AAoN;G0Ig3>2H9?JX*XVk=dE6lI>JZ%ZrLRQMJr$ictAp3nvb4 zP23vnK>{oOIVVF$Od?YM-j?Q?k6639q95Bi(9U^#^oz<-i%a@wAYRj&SpE?grY#S~5BCgV+Evv!^c&tSfv`GclDRx~X|KLYtm}mn1s6_)iJve&^(B#R za6}^7#N42`#yR76_N7-63BHQlz+98~h$PwxH3+?WG@2*BAt{xH>lq{D>8}zf=hOW9 zirjHD$RS+1Ehzny0}>G#9wSwEBjU4fg#~;#d0cJ&u zyJQX5a^k&OPa&`Uc|*Pm3!NoJ9Jxb$@VskFop)l(TsdrM&T%R|nIBC5vSYLHWWl$5 zpFX%NjLS)~E1H0oXi|=6INn9#mFDD}hAL-olEO;HgAQs^4e?Piv}RHam+5t~;v&cKw6(EHuKQ&2OQIV76e(Hqdr{BFSmBu{^CVnM5>r_}y5v;(F)2qCpgGZ_x( z&UA6yKCUH@HWaJjYs__$w0od=SB3);J^2-|D@mU1?eDT}U?5(gi;8nZ%XRQ(nFH0# z_${HPhE9N+&{gUU%_WgjRA~LSI)vmPzq0j0Eol^~HWeN!AjgQ=d->}2=we;5?E0=5 zjXU$o>=Bk!$64nAEhjK2(o~r1GrP!+M&Y?n*XZJIvh;Bn$IKEv^cA}MbIiNSg<2Fn zKiL9>HUV-BTykbQ-E=?}Gi805X)EIPdD3+fwqp`VPL3p7y-0BdX%2oCj;Nn}gxE3# z{$))OB%)Zf(WTaER6CRO{abBdv{gYc(LO;2pk-l}7SJQ>H6yV&?UiIxFBZnCBbz}e z4Vg5m!pWFSTH+gLXYMHp`#h+=9aIfGsqU{~H)SUGaAQ|*e5{=BE@R6}37VOzg0jG|UdJ9?$U;&p9um^_b?+dtpd$0eo#qW=|dxWCqzqY*8H_#pu=WC$~Qeb4$ z4u)d1yeS1`Br@yH0}rqf=2j2>vA>HJ$p?CjtnDW2gH)7=*AEZi1G;=vNzdeT&inLZ z&s9$5FVCLOuXK^9DGlGeQIH|>7ecsW$jsRe4o#J$$&2#M=%uA0v(xlGE^4Iya=lfP zx6u{~0gdc2Y-y(|SWq)`m9%meSgabjPfNX94Be%#GqetE-0Yp`|7n0mxY9kcDtwzM zpG2w?Sw~%v<=wmzqGsKG+>$C1y3d5UrNa)|A{%8)`8=z!HIsP117^4zw}0Fgb-lrE zaMyRTIg5hvcT9cTm&%_fbXBz9TCwozX)4i~+1e;#zhOwCPfHZRmqQdu2|EpH#vl&x<*fdf`tvTSeu&=iG-`MLeOzu_c9 zpdJ`Vp}8t<$f5KA_FeRhE#4<+GP%-x@QIt`k6mgQLEPDQ#DDNV1Qd)Y6n0+544qbYFn_hddWTgkGoUhy75a65*EVCGUVmX=!_r9f|#<^IF@dqiYMlgIZNbJ3ZkN8^O z!ATJR{9-3qm>{dACt#;53}B}#*Dsy|dmD2j1IOQy15S^TmhTim4H^Rj74hVM)0K13 z4vLN9wE0>goH~aD^IDiYfzc`i76j^9N|lW6;nfqxCTa=eyvQca(Ow$o>V@3|<5LwNia0`+~QW+JYjFz_nM=R+OOjQ4e@q^}!i@Ss=g3gFa~S5B~1TJgej zm&_RO8GZUK;5s{e;3I4~0@7U|G^$rQlo(8o__49xT<8^yrY2j(g^wQc24oBiLxU-P zLUF)@DL^lLl^cfQ2{pv1^>O_|Zlnl|K54+~TDaoSLP(`;cN}ztIPa%>( ztpseD_?N@>zxC_^o6-MS%YOT%f;}W8B#@IEx-iB4_I z4*ZSz*j*#>7*NR>KnwkFBKuiz0Z;BP0EBF89DmpZ*vjgeSp%{zf0_fltedI7p>Zw% zZw7F@{)r1XQvoFW`~q48Ao^fwqi6UZnZcKUgEm|a2!Li10MwV|FMwQtxX3>N0^S?0 zZh}sZrT{r+GlQRkCodV=Q$yDw0shRm0KW#Jzc8c%+UFk_{+N!vgqxP5U(N==3<3_u zzrbx^{39Hoz9#lY4h}K^A8;e<|F|!_gg2N!O0fepB_iw}2?9SECIOGqFKP==qPBE$ z0LZ_HnOPbM8W;c=Wc94|O#T}IXtDdU7NEfu01f`v&JjoOO9F9wJ%9U3N$@bSx)9^ueKL;8Jhzd|sz}~Ar6>j2xfK~-`Jb>ZnkG}V7+?@k8hY|qVBH#c3 z{i%=we!(x=MDjnq?$>zfrI|tq+4+5v1S&p40=t zU_$t(+5sLB)_=nLb?x%q7@Ac8HwXyu?EttVe=6P*zeJO|S z`U~sEm(VZ0@_s^tX#ErPU!C(_qP{ep`H6~X1dvGo*E9N$7Bnvbe*paGWk1Rfo0*@* z1_1b{_WhBR_VVJFrZYb;&iMPq|MC6!?{mSI@Gp%-e!^3l{;h8Rf&ZQFh?hh!h5vsN zZCL(|=uh?kFE#)#;a@7)|Acq7{wMesQ{|ViFNNEG!p__L6YQU(3@hD>y)Uc(Qgiet5rg~h68+IU|5hMKMP~`e~-Ytkp=^N2mQ!hWCD5zh_{3S{uT)6 F{{ewv_hSG6 literal 0 HcmV?d00001 diff --git a/ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.properties b/ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1083004 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Aug 22 15:11:42 CDT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/ExternalLibs/Parse-SDK-Android/gradlew b/ExternalLibs/Parse-SDK-Android/gradlew new file mode 100755 index 0000000..4453cce --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/ExternalLibs/Parse-SDK-Android/gradlew.bat b/ExternalLibs/Parse-SDK-Android/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ExternalLibs/Parse-SDK-Android/scripts/publish_snapshot.sh b/ExternalLibs/Parse-SDK-Android/scripts/publish_snapshot.sh new file mode 100755 index 0000000..3efff92 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/scripts/publish_snapshot.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Publishes SNAPSHOTs + +REPO_SLUG=parse-community/Parse-SDK-Android +BRANCH=master + +set -e + +if [ "$TRAVIS_REPO_SLUG" != "$REPO_SLUG" ]; then + echo "Skipping publishing SNAPSHOT: wrong repository. Expected '$REPO_SLUG' but was '$TRAVIS_REPO_SLUG'" +elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo "Skipping publishing SNAPSHOT: was PR" +elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then + echo "Skipping publishing SNAPSHOT: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'" +else + echo "Publishing SNAPSHOT..." + ./gradlew artifactoryPublish + echo "SNAPSHOT published!" +fi diff --git a/ExternalLibs/Parse-SDK-Android/settings.gradle b/ExternalLibs/Parse-SDK-Android/settings.gradle new file mode 100644 index 0000000..7cd3dd4 --- /dev/null +++ b/ExternalLibs/Parse-SDK-Android/settings.gradle @@ -0,0 +1,2 @@ +include ':Parse' + diff --git a/ExternalLibs/ParseLiveQuery-Android/.gitignore b/ExternalLibs/ParseLiveQuery-Android/.gitignore new file mode 100644 index 0000000..8cb1c31 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/.gitignore @@ -0,0 +1,40 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Android Studio +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Gradle +.gradle +build + +# Other +.metadata +*/bin/* +*/gen/* +testData +testCache +server.config + +# Jacoco +jacoco.exec + diff --git a/ExternalLibs/ParseLiveQuery-Android/.travis.yml b/ExternalLibs/ParseLiveQuery-Android/.travis.yml new file mode 100644 index 0000000..49c6ca6 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/.travis.yml @@ -0,0 +1,42 @@ +branches: + only: + - master + - /^\d+\.\d+\.\d+$/ # regex +language: android +sudo: false +jdk: + - oraclejdk8 + +android: + components: + - tools # to get the new `repository-11.xml` + - platform-tools + - build-tools-25.0.2 + - android-25 + - doc-25 + - extra-google-m2repository + - extra-android-m2repository + licenses: + - 'android-sdk-license-.+' + +before_install: + - pip install --user codecov + +script: + - ./gradlew clean testDebugUnitTest jacocoTestReport --info + +after_success: + - codecov + - ./scripts/publish_snapshot.sh + +cache: + directories: + - $HOME/.gradle + - $HOME/.m2/repository +deploy: + provider: script + script: ./gradlew bintrayUpload + skip_cleanup: true + on: + branch: master + tags: true \ No newline at end of file diff --git a/ExternalLibs/ParseLiveQuery-Android/CONTRIBUTING.md b/ExternalLibs/ParseLiveQuery-Android/CONTRIBUTING.md new file mode 100644 index 0000000..4c0eca2 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to Parse LiveQuery for Android +We want to make contributing to this project as easy and transparent as possible. + +## Code of Conduct +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. + +## Our Development Process +Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. + +### `master` is unsafe +Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. + +### Pull Requests +We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +1. Fork the repo and create your branch from `master`. +4. Add unit tests for any new code you add. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +### Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Bugs +Although we try to keep developing on Parse easy, you still may run into some issues. General questions should be asked on [Google Groups][google-group], technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. + +### Known Issues +We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues +Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: + +* A description. What did you expect to happen and what actually happened? Why do you think that was wrong? +* A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. +* What version does this reproduce on? What version did it last work on? +* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. +* Anything else you find relevant. + +### Security Bugs +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. + +## Style Guide +We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following: + +* Most importantly, match the existing code style as much as possible. +* Try to keep lines under 100 characters, if possible. + +## License +By contributing to Parse Android Parse LiveQuery, you agree that your contributions will be licensed under its license. + + [google-group]: https://groups.google.com/forum/#!forum/parse-developers + [stack-overflow]: http://stackoverflow.com/tags/parse.com + [bug-reports]: https://www.parse.com/help#report + [rest-api]: https://www.parse.com/docs/rest/guide + [network-debugging-tool]: https://github.com/ParsePlatform/ParseInterceptors-Android/wiki + [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ + [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg + [tests-dir]: /Parse/src/test/java/com/parse diff --git a/ExternalLibs/ParseLiveQuery-Android/LICENSE b/ExternalLibs/ParseLiveQuery-Android/LICENSE new file mode 100644 index 0000000..e4acc79 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/LICENSE @@ -0,0 +1,34 @@ +BSD License + +For Parse LiveQueryClient for Android software + +Copyright (c) 2015-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/ExternalLibs/ParseLiveQuery-Android/PATENTS b/ExternalLibs/ParseLiveQuery-Android/PATENTS new file mode 100644 index 0000000..76c62c6 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/PATENTS @@ -0,0 +1,37 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the Parse Android LiveQuery Client software distributed by Parse, LLC. + +Parse, LLC. ("Parse") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Parse’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Parse or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Parse or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Parse or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Parse that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/build.gradle b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/build.gradle new file mode 100644 index 0000000..4e3c8cc --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.github.kt3k.coveralls' + +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.1' + } +} + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + //buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName project.version + consumerProguardFiles 'release-proguard.pro' + } + + lintOptions { + abortOnError false + } + + buildTypes { + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + //compile 'com.parse:parse-android:1.14.1' + compile "com.squareup.okhttp3:okhttp:$okhttpVersion" + compile 'com.parse.bolts:bolts-tasks:1.4.0' + testCompile 'org.robolectric:robolectric:3.3.1' + testCompile 'org.skyscreamer:jsonassert:1.5.0' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + implementation project(':Parse') +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/release-proguard.pro b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/release-proguard.pro new file mode 100644 index 0000000..fcd0a5d --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/release-proguard.pro @@ -0,0 +1,29 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Keep source file names, line numbers, and Parse class/method names for easier debugging +-keepattributes SourceFile,LineNumberTable +-keepnames class com.parse.** { *; } + +# Required for Parse +-keepattributes *Annotation* +-keepattributes Signature +-dontwarn android.net.SSLCertificateSocketFactory +-dontwarn android.app.Notification +-dontwarn com.squareup.** +-dontwarn okio.** diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/AndroidManifest.xml b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e1d3400 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ClientOperation.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ClientOperation.java new file mode 100644 index 0000000..2383a08 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ClientOperation.java @@ -0,0 +1,10 @@ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +/* package */ abstract class ClientOperation { + + abstract JSONObject getJSONObjectRepresentation() throws JSONException; + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ConnectClientOperation.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ConnectClientOperation.java new file mode 100644 index 0000000..76b08ad --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ConnectClientOperation.java @@ -0,0 +1,24 @@ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +/* package */ class ConnectClientOperation extends ClientOperation { + + private final String applicationId; + private final String sessionToken; + + /* package */ ConnectClientOperation(String applicationId, String sessionToken) { + this.applicationId = applicationId; + this.sessionToken = sessionToken; + } + + @Override + /* package */ JSONObject getJSONObjectRepresentation() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "connect"); + jsonObject.put("applicationId", applicationId); + jsonObject.put("sessionToken", sessionToken); + return jsonObject; + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/LiveQueryException.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/LiveQueryException.java new file mode 100644 index 0000000..1f5be2a --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/LiveQueryException.java @@ -0,0 +1,101 @@ +package com.parse; + +import java.util.Locale; + +public abstract class LiveQueryException extends Exception { + + private LiveQueryException() { + super(); + } + + private LiveQueryException(String detailMessage) { + super(detailMessage); + } + + private LiveQueryException(String detailMessage, Throwable cause) { + super(detailMessage, cause); + } + + private LiveQueryException(Throwable cause) { + super(cause); + } + + /** + * An error that is reported when any other unknown {@link RuntimeException} occurs unexpectedly. + */ + public static class UnknownException extends LiveQueryException { + /* package */ UnknownException(String detailMessage, RuntimeException cause) { + super(detailMessage, cause); + } + } + + /** + * An error that is reported when the server returns a response that cannot be parsed. + */ + public static class InvalidResponseException extends LiveQueryException { + /* package */ InvalidResponseException(String response) { + super(response); + } + } + + /** + * An error that is reported when the server does not accept a query we've sent to it. + */ + public static class InvalidQueryException extends LiveQueryException { + + } + + /** + * An error that is reported when the server returns valid JSON, but it doesn't match the format we expect. + */ + public static class InvalidJSONException extends LiveQueryException { + // JSON used for matching. + private final String json; + /// Key that was expected to match. + private final String expectedKey; + + /* package */ InvalidJSONException(String json, String expectedKey) { + super(String.format(Locale.US, "Invalid JSON; expectedKey: %s, json: %s", expectedKey, json)); + this.json = json; + this.expectedKey = expectedKey; + } + + public String getJson() { + return json; + } + + public String getExpectedKey() { + return expectedKey; + } + } + + /** + * An error that is reported when the live query server encounters an internal error. + */ + public static class ServerReportedException extends LiveQueryException { + + private final int code; + private final String error; + private final boolean reconnect; + + public ServerReportedException(int code, String error, boolean reconnect) { + super(String.format(Locale.US, "Server reported error; code: %d, error: %s, reconnect: %b", code, error, reconnect)); + this.code = code; + this.error = error; + this.reconnect = reconnect; + } + + public int getCode() { + return code; + } + + public String getError() { + return error; + } + + public boolean isReconnect() { + return reconnect; + } + } + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/OkHttp3SocketClientFactory.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/OkHttp3SocketClientFactory.java new file mode 100644 index 0000000..71c7c28 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/OkHttp3SocketClientFactory.java @@ -0,0 +1,119 @@ +package com.parse; + +import android.util.Log; + +import java.net.URI; +import java.util.Locale; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +/* package */ public class OkHttp3SocketClientFactory implements WebSocketClientFactory { + + OkHttpClient mClient; + + public OkHttp3SocketClientFactory(OkHttpClient client) { + mClient = client; + } + + public OkHttp3SocketClientFactory() { + mClient = new OkHttpClient(); + } + + @Override + public WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl) { + return new OkHttp3WebSocketClient(mClient, webSocketClientCallback, hostUrl); + } + + static class OkHttp3WebSocketClient implements WebSocketClient { + + private static final String LOG_TAG = "OkHttpWebSocketClient"; + + private final WebSocketClientCallback webSocketClientCallback; + private WebSocket webSocket; + private State state = State.NONE; + private final OkHttpClient client; + private final String url; + private final int STATUS_CODE = 1000; + private final String CLOSING_MSG = "User invoked close"; + + private final WebSocketListener handler = new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + setState(State.CONNECTED); + webSocketClientCallback.onOpen(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + webSocketClientCallback.onMessage(text); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + Log.w(LOG_TAG, String.format(Locale.US, + "Socket got into inconsistent state and received %s instead.", + bytes.toString())); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + setState(State.DISCONNECTED); + webSocketClientCallback.onClose(); + } + + @Override + public void onFailure(okhttp3.WebSocket webSocket, Throwable t, Response response) { + webSocketClientCallback.onError(t); + } + }; + + private OkHttp3WebSocketClient(OkHttpClient okHttpClient, + WebSocketClientCallback webSocketClientCallback, URI hostUrl) { + client = okHttpClient; + this.webSocketClientCallback = webSocketClientCallback; + url = hostUrl.toString(); + } + + @Override + public synchronized void open() { + if (State.NONE == state) { + // OkHttp3 connects as soon as the socket is created so do it here. + Request request = new Request.Builder() + .url(url) + .build(); + + webSocket = client.newWebSocket(request, handler); + setState(State.CONNECTING); + } + } + + @Override + public synchronized void close() { + setState(State.DISCONNECTING); + webSocket.close(STATUS_CODE, CLOSING_MSG); + } + + @Override + public void send(String message) { + if (state == State.CONNECTED) { + webSocket.send(message); + } + } + + @Override + public State getState() { + return state; + } + + private synchronized void setState(State newState) { + this.state = newState; + this.webSocketClientCallback.stateChanged(); + } + } + +} \ No newline at end of file diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClient.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClient.java new file mode 100644 index 0000000..2ec846d --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClient.java @@ -0,0 +1,47 @@ +package com.parse; + +import java.net.URI; +import java.util.concurrent.Executor; + +public interface ParseLiveQueryClient { + SubscriptionHandling subscribe(ParseQuery query); + + void unsubscribe(final ParseQuery query); + + void unsubscribe(final ParseQuery query, final SubscriptionHandling subscriptionHandling); + + void connectIfNeeded(); + + void reconnect(); + + void disconnect(); + + void registerListener(ParseLiveQueryClientCallbacks listener); + + void unregisterListener(ParseLiveQueryClientCallbacks listener); + + class Factory { + + public static ParseLiveQueryClient getClient() { + return new ParseLiveQueryClientImpl(); + } + + public static ParseLiveQueryClient getClient(WebSocketClientFactory webSocketClientFactory) { + return new ParseLiveQueryClientImpl(webSocketClientFactory); + } + + public static ParseLiveQueryClient getClient(URI uri) { + return new ParseLiveQueryClientImpl(uri); + } + + public static ParseLiveQueryClient getClient(URI uri, WebSocketClientFactory webSocketClientFactory) { + return new ParseLiveQueryClientImpl(uri, webSocketClientFactory); + } + + /* package */ + static ParseLiveQueryClient getClient(URI uri, WebSocketClientFactory webSocketClientFactory, Executor taskExecutor) { + return new ParseLiveQueryClientImpl(uri, webSocketClientFactory, taskExecutor); + } + + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientCallbacks.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientCallbacks.java new file mode 100644 index 0000000..c8275fa --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientCallbacks.java @@ -0,0 +1,11 @@ +package com.parse; + +public interface ParseLiveQueryClientCallbacks { + void onLiveQueryClientConnected(ParseLiveQueryClient client); + + void onLiveQueryClientDisconnected(ParseLiveQueryClient client, boolean userInitiated); + + void onLiveQueryError(ParseLiveQueryClient client, LiveQueryException reason); + + void onSocketError(ParseLiveQueryClient client, Throwable reason); +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientImpl.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientImpl.java new file mode 100644 index 0000000..73380a8 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/ParseLiveQueryClientImpl.java @@ -0,0 +1,435 @@ +package com.parse; + +import android.util.Log; +import android.util.SparseArray; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +import bolts.Continuation; +import bolts.Task; +import okhttp3.OkHttpClient; + +import static com.parse.Parse.checkInit; + +/* package */ class ParseLiveQueryClientImpl implements ParseLiveQueryClient { + + private static final String LOG_TAG = "ParseLiveQueryClient"; + + private final Executor taskExecutor; + private final String applicationId; + private final String clientKey; + private final SparseArray> subscriptions = new SparseArray<>(); + private final URI uri; + private final WebSocketClientFactory webSocketClientFactory; + private final WebSocketClient.WebSocketClientCallback webSocketClientCallback; + + private final List mCallbacks = new ArrayList<>(); + + private WebSocketClient webSocketClient; + private int requestIdCount = 1; + private boolean userInitiatedDisconnect = false; + private boolean hasReceivedConnected = false; + + /* package */ ParseLiveQueryClientImpl() { + this(getDefaultUri()); + } + + /* package */ ParseLiveQueryClientImpl(URI uri) { + this(uri, new OkHttp3SocketClientFactory(new OkHttpClient()), Task.BACKGROUND_EXECUTOR); + } + + /* package */ ParseLiveQueryClientImpl(URI uri, WebSocketClientFactory webSocketClientFactory) { + this(uri, webSocketClientFactory, Task.BACKGROUND_EXECUTOR); + } + + /* package */ ParseLiveQueryClientImpl(WebSocketClientFactory webSocketClientFactory) { + this(getDefaultUri(), webSocketClientFactory, Task.BACKGROUND_EXECUTOR); + } + + /* package */ ParseLiveQueryClientImpl(URI uri, WebSocketClientFactory webSocketClientFactory, Executor taskExecutor) { + checkInit(); + this.uri = uri; + this.applicationId = ParsePlugins.get().applicationId(); + this.clientKey = ParsePlugins.get().clientKey(); + this.webSocketClientFactory = webSocketClientFactory; + this.taskExecutor = taskExecutor; + this.webSocketClientCallback = getWebSocketClientCallback(); + } + + private static URI getDefaultUri() { + URL serverUrl = ParseRESTCommand.server; + if (serverUrl == null) return null; + String url = serverUrl.toString(); + if (serverUrl.getProtocol().equals("https")) { + url = url.replaceFirst("https", "wss"); + } else { + url = url.replaceFirst("http", "ws"); + } + try { + return new URI(url); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } + + @Override + public SubscriptionHandling subscribe(ParseQuery query) { + int requestId = requestIdGenerator(); + Subscription subscription = new Subscription<>(requestId, query); + subscriptions.append(requestId, subscription); + + if (isConnected()) { + sendSubscription(subscription); + } else if (userInitiatedDisconnect) { + Log.w(LOG_TAG, "Warning: The client was explicitly disconnected! You must explicitly call .reconnect() in order to process your subscriptions."); + } else { + connectIfNeeded(); + } + + return subscription; + } + + public void connectIfNeeded() { + switch (getWebSocketState()) { + case CONNECTED: + // nothing to do + break; + case CONNECTING: + // just wait for it to finish connecting + break; + + case NONE: + case DISCONNECTING: + case DISCONNECTED: + reconnect(); + break; + + default: + + break; + } + } + + @Override + public void unsubscribe(final ParseQuery query) { + if (query != null) { + for (int i = 0; i < subscriptions.size(); i++) { + Subscription subscription = subscriptions.valueAt(i); + if (query.equals(subscription.getQuery())) { + sendUnsubscription(subscription); + } + } + } + } + + @Override + public void unsubscribe(final ParseQuery query, final SubscriptionHandling subscriptionHandling) { + if (query != null && subscriptionHandling != null) { + for (int i = 0; i < subscriptions.size(); i++) { + Subscription subscription = subscriptions.valueAt(i); + if (query.equals(subscription.getQuery()) && subscriptionHandling.equals(subscription)) { + sendUnsubscription(subscription); + } + } + } + } + + @Override + public void reconnect() { + if (webSocketClient != null) { + webSocketClient.close(); + } + + userInitiatedDisconnect = false; + hasReceivedConnected = false; + webSocketClient = webSocketClientFactory.createInstance(webSocketClientCallback, uri); + webSocketClient.open(); + } + + @Override + public void disconnect() { + if (webSocketClient != null) { + webSocketClient.close(); + webSocketClient = null; + } + + userInitiatedDisconnect = true; + hasReceivedConnected = false; + } + + @Override + public void registerListener(ParseLiveQueryClientCallbacks listener) { + mCallbacks.add(listener); + } + + @Override + public void unregisterListener(ParseLiveQueryClientCallbacks listener) { + mCallbacks.remove(listener); + } + + // Private methods + + private synchronized int requestIdGenerator() { + return requestIdCount++; + } + + private WebSocketClient.State getWebSocketState() { + WebSocketClient.State state = webSocketClient == null ? null : webSocketClient.getState(); + return state == null ? WebSocketClient.State.NONE : state; + } + + private boolean isConnected() { + return hasReceivedConnected && inAnyState(WebSocketClient.State.CONNECTED); + } + + private boolean inAnyState(WebSocketClient.State... states) { + return Arrays.asList(states).contains(getWebSocketState()); + } + + private Task handleOperationAsync(final String message) { + return Task.call(new Callable() { + public Void call() throws Exception { + parseMessage(message); + return null; + } + }, taskExecutor); + } + + private Task sendOperationAsync(final ClientOperation clientOperation) { + return Task.call(new Callable() { + public Void call() throws Exception { + JSONObject jsonEncoded = clientOperation.getJSONObjectRepresentation(); + String jsonString = jsonEncoded.toString(); + if (Parse.getLogLevel() <= Parse.LOG_LEVEL_DEBUG) { + Log.d(LOG_TAG, "Sending over websocket: " + jsonString); + } + webSocketClient.send(jsonString); + return null; + } + }, taskExecutor); + } + + private void parseMessage(String message) throws LiveQueryException { + try { + JSONObject jsonObject = new JSONObject(message); + String rawOperation = jsonObject.getString("op"); + + switch (rawOperation) { + case "connected": + hasReceivedConnected = true; + dispatchConnected(); + Log.v(LOG_TAG, "Connected, sending pending subscription"); + for (int i = 0; i < subscriptions.size(); i++) { + sendSubscription(subscriptions.valueAt(i)); + } + break; + case "redirect": + String url = jsonObject.getString("url"); + // TODO: Handle redirect. + Log.d(LOG_TAG, "Redirect is not yet handled"); + break; + case "subscribed": + handleSubscribedEvent(jsonObject); + break; + case "unsubscribed": + handleUnsubscribedEvent(jsonObject); + break; + case "enter": + handleObjectEvent(Subscription.Event.ENTER, jsonObject); + break; + case "leave": + handleObjectEvent(Subscription.Event.LEAVE, jsonObject); + break; + case "update": + handleObjectEvent(Subscription.Event.UPDATE, jsonObject); + break; + case "create": + handleObjectEvent(Subscription.Event.CREATE, jsonObject); + break; + case "delete": + handleObjectEvent(Subscription.Event.DELETE, jsonObject); + break; + case "error": + handleErrorEvent(jsonObject); + break; + default: + throw new LiveQueryException.InvalidResponseException(message); + } + } catch (JSONException e) { + throw new LiveQueryException.InvalidResponseException(message); + } + } + + private void dispatchConnected() { + for (ParseLiveQueryClientCallbacks callback : mCallbacks) { + callback.onLiveQueryClientConnected(this); + } + } + + private void dispatchDisconnected() { + for (ParseLiveQueryClientCallbacks callback : mCallbacks) { + callback.onLiveQueryClientDisconnected(this, userInitiatedDisconnect); + } + } + + + private void dispatchServerError(LiveQueryException exc) { + for (ParseLiveQueryClientCallbacks callback : mCallbacks) { + callback.onLiveQueryError(this, exc); + } + } + + private void dispatchSocketError(Throwable reason) { + userInitiatedDisconnect = false; + + for (ParseLiveQueryClientCallbacks callback : mCallbacks) { + callback.onSocketError(this, reason); + } + + dispatchDisconnected(); + } + + private void handleSubscribedEvent(JSONObject jsonObject) throws JSONException { + final int requestId = jsonObject.getInt("requestId"); + final Subscription subscription = subscriptionForRequestId(requestId); + if (subscription != null) { + subscription.didSubscribe(subscription.getQuery()); + } + } + + private void handleUnsubscribedEvent(JSONObject jsonObject) throws JSONException { + final int requestId = jsonObject.getInt("requestId"); + final Subscription subscription = subscriptionForRequestId(requestId); + if (subscription != null) { + subscription.didUnsubscribe(subscription.getQuery()); + subscriptions.remove(requestId); + } + } + + private void handleObjectEvent(Subscription.Event event, JSONObject jsonObject) throws JSONException { + final int requestId = jsonObject.getInt("requestId"); + final Subscription subscription = subscriptionForRequestId(requestId); + if (subscription != null) { + T object = ParseObject.fromJSON(jsonObject.getJSONObject("object"), subscription.getQueryState().className(), ParseDecoder.get(), subscription.getQueryState().selectedKeys()); + subscription.didReceive(event, subscription.getQuery(), object); + } + } + + private void handleErrorEvent(JSONObject jsonObject) throws JSONException { + int requestId = jsonObject.getInt("requestId"); + int code = jsonObject.getInt("code"); + String error = jsonObject.getString("error"); + Boolean reconnect = jsonObject.getBoolean("reconnect"); + final Subscription subscription = subscriptionForRequestId(requestId); + LiveQueryException exc = new LiveQueryException.ServerReportedException(code, error, reconnect); + + if (subscription != null) { + subscription.didEncounter(exc, subscription.getQuery()); + } + + dispatchServerError(exc); + } + + private Subscription subscriptionForRequestId(int requestId) { + //noinspection unchecked + return (Subscription) subscriptions.get(requestId); + } + + private void sendSubscription(final Subscription subscription) { + ParseUser.getCurrentSessionTokenAsync().onSuccess(new Continuation() { + @Override + public Void then(Task task) throws Exception { + String sessionToken = task.getResult(); + SubscribeClientOperation op = new SubscribeClientOperation<>(subscription.getRequestId(), subscription.getQueryState(), sessionToken); + + // dispatch errors + sendOperationAsync(op).continueWith(new Continuation() { + public Void then(Task task) { + Exception error = task.getError(); + if (error != null) { + if (error instanceof RuntimeException) { + subscription.didEncounter(new LiveQueryException.UnknownException( + "Error when subscribing", (RuntimeException) error), subscription.getQuery()); + } + } + return null; + } + }); + return null; + } + }); + } + + private void sendUnsubscription(Subscription subscription) { + sendOperationAsync(new UnsubscribeClientOperation(subscription.getRequestId())); + } + + private WebSocketClient.WebSocketClientCallback getWebSocketClientCallback() { + return new WebSocketClient.WebSocketClientCallback() { + @Override + public void onOpen() { + hasReceivedConnected = false; + Log.v(LOG_TAG, "Socket opened"); + ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + String sessionToken = task.getResult(); + return sendOperationAsync(new ConnectClientOperation(applicationId, sessionToken)); + } + }).continueWith(new Continuation() { + public Void then(Task task) { + Exception error = task.getError(); + if (error != null) { + Log.e(LOG_TAG, "Error when connection client", error); + } + return null; + } + }); + } + + @Override + public void onMessage(String message) { + Log.v(LOG_TAG, "Socket onMessage " + message); + handleOperationAsync(message).continueWith(new Continuation() { + public Void then(Task task) { + Exception error = task.getError(); + if (error != null) { + Log.e(LOG_TAG, "Error handling message", error); + } + return null; + } + }); + } + + @Override + public void onClose() { + Log.v(LOG_TAG, "Socket onClose"); + hasReceivedConnected = false; + dispatchDisconnected(); + } + + @Override + public void onError(Throwable exception) { + Log.e(LOG_TAG, "Socket onError", exception); + hasReceivedConnected = false; + dispatchSocketError(exception); + } + + @Override + public void stateChanged() { + Log.v(LOG_TAG, "Socket stateChanged"); + } + }; + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscribeClientOperation.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscribeClientOperation.java new file mode 100644 index 0000000..e9387d4 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscribeClientOperation.java @@ -0,0 +1,38 @@ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +/* package */ class SubscribeClientOperation extends ClientOperation { + + private final int requestId; + private final ParseQuery.State state; + private final String sessionToken; + + /* package */ SubscribeClientOperation(int requestId, ParseQuery.State state, String sessionToken) { + this.requestId = requestId; + this.state = state; + this.sessionToken = sessionToken; + } + + @Override + /* package */ JSONObject getJSONObjectRepresentation() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "subscribe"); + jsonObject.put("requestId", requestId); + jsonObject.put("sessionToken", sessionToken); + + JSONObject queryJsonObject = new JSONObject(); + queryJsonObject.put("className", state.className()); + + // TODO: add support for fields + // https://github.com/ParsePlatform/parse-server/issues/3671 + + PointerEncoder pointerEncoder = PointerEncoder.get(); + queryJsonObject.put("where", pointerEncoder.encode(state.constraints())); + + jsonObject.put("query", queryJsonObject); + + return jsonObject; + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/Subscription.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/Subscription.java new file mode 100644 index 0000000..d400cde --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/Subscription.java @@ -0,0 +1,120 @@ +package com.parse; + +import java.util.ArrayList; +import java.util.List; + +/* package */ class Subscription implements SubscriptionHandling { + + private final List> handleEventsCallbacks = new ArrayList<>(); + private final List> handleErrorCallbacks = new ArrayList<>(); + private final List> handleSubscribeCallbacks = new ArrayList<>(); + private final List> handleUnsubscribeCallbacks = new ArrayList<>(); + + private final int requestId; + private final ParseQuery query; + private final ParseQuery.State state; + + /* package */ Subscription(int requestId, ParseQuery query) { + this.requestId = requestId; + this.query = query; + this.state = query.getBuilder().build(); + } + + @Override + public Subscription handleEvents(HandleEventsCallback callback) { + handleEventsCallbacks.add(callback); + return this; + } + + @Override + public Subscription handleEvent(final Event event, final HandleEventCallback callback) { + return handleEvents(new HandleEventsCallback() { + @Override + public void onEvents(ParseQuery query, Event callbackEvent, T object) { + if (callbackEvent == event) { + callback.onEvent(query, object); + } + } + }); + } + + @Override + public Subscription handleError(HandleErrorCallback callback) { + handleErrorCallbacks.add(callback); + return this; + } + + @Override + public Subscription handleSubscribe(HandleSubscribeCallback callback) { + handleSubscribeCallbacks.add(callback); + return this; + } + + @Override + public Subscription handleUnsubscribe(HandleUnsubscribeCallback callback) { + handleUnsubscribeCallbacks.add(callback); + return this; + } + + @Override + public int getRequestId() { + return requestId; + } + + /* package */ ParseQuery getQuery() { + return query; + } + + /* package */ ParseQuery.State getQueryState() { + return state; + } + + /** + * Tells the handler that an event has been received from the live query server. + * + * @param event The event that has been received from the server. + * @param query The query that the event occurred on. + */ + /* package */ void didReceive(Event event, ParseQuery query, T object) { + for (HandleEventsCallback handleEventsCallback : handleEventsCallbacks) { + handleEventsCallback.onEvents(query, event, object); + } + } + + /** + * Tells the handler that an error has been received from the live query server. + * + * @param error The error that the server has encountered. + * @param query The query that the error occurred on. + */ + /* package */ void didEncounter(LiveQueryException error, ParseQuery query) { + for (HandleErrorCallback handleErrorCallback : handleErrorCallbacks) { + handleErrorCallback.onError(query, error); + } + } + + /** + * Tells the handler that a query has been successfully registered with the server. + * - note: This may be invoked multiple times if the client disconnects/reconnects. + * + * @param query The query that has been subscribed. + */ + /* package */ void didSubscribe(ParseQuery query) { + for (HandleSubscribeCallback handleSubscribeCallback : handleSubscribeCallbacks) { + handleSubscribeCallback.onSubscribe(query); + } + } + + /** + * Tells the handler that a query has been successfully deregistered from the server. + * - note: This is not called unless `unregister()` is explicitly called. + * + * @param query The query that has been unsubscribed. + */ + /* package */ void didUnsubscribe(ParseQuery query) { + for (HandleUnsubscribeCallback handleUnsubscribeCallback : handleUnsubscribeCallbacks) { + handleUnsubscribeCallback.onUnsubscribe(query); + } + } + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscriptionHandling.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscriptionHandling.java new file mode 100644 index 0000000..e8514d5 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/SubscriptionHandling.java @@ -0,0 +1,72 @@ +package com.parse; + +public interface SubscriptionHandling { + + /** + * Register a callback for when an event occurs. + * + * @param callback The callback to register. + * @return The same SubscriptionHandling, for easy chaining. + */ + SubscriptionHandling handleEvents(Subscription.HandleEventsCallback callback); + + /** + * Register a callback for when an event occurs. + * + * @param event The event type to handle. You should pass one of the enum cases in Event + * @param callback The callback to register. + * @return The same SubscriptionHandling, for easy chaining. + */ + SubscriptionHandling handleEvent(Subscription.Event event, Subscription.HandleEventCallback callback); + + /** + * Register a callback for when an event occurs. + * + * @param callback The callback to register. + * @return The same SubscriptionHandling, for easy chaining. + */ + SubscriptionHandling handleError(Subscription.HandleErrorCallback callback); + + /** + * Register a callback for when a client succesfully subscribes to a query. + * + * @param callback The callback to register. + * @return The same SubscriptionHandling, for easy chaining. + */ + SubscriptionHandling handleSubscribe(Subscription.HandleSubscribeCallback callback); + + /** + * Register a callback for when a query has been unsubscribed. + * + * @param callback The callback to register. + * @return The same SubscriptionHandling, for easy chaining. + */ + SubscriptionHandling handleUnsubscribe(Subscription.HandleUnsubscribeCallback callback); + + int getRequestId(); + + interface HandleEventsCallback { + void onEvents(ParseQuery query, Subscription.Event event, T object); + } + + interface HandleEventCallback { + void onEvent(ParseQuery query, T object); + } + + interface HandleErrorCallback { + void onError(ParseQuery query, LiveQueryException exception); + } + + interface HandleSubscribeCallback { + void onSubscribe(ParseQuery query); + } + + interface HandleUnsubscribeCallback { + void onUnsubscribe(ParseQuery query); + } + + enum Event { + CREATE, ENTER, UPDATE, LEAVE, DELETE + } + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/UnsubscribeClientOperation.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/UnsubscribeClientOperation.java new file mode 100644 index 0000000..b11ff48 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/UnsubscribeClientOperation.java @@ -0,0 +1,21 @@ +package com.parse; + +import org.json.JSONException; +import org.json.JSONObject; + +/* package */ class UnsubscribeClientOperation extends ClientOperation { + + private final int requestId; + + /* package */ UnsubscribeClientOperation(int requestId) { + this.requestId = requestId; + } + + @Override + /* package */ JSONObject getJSONObjectRepresentation() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "unsubscribe"); + jsonObject.put("requestId", requestId); + return jsonObject; + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClient.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClient.java new file mode 100644 index 0000000..2c7fb0a --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClient.java @@ -0,0 +1,27 @@ +package com.parse; + +/* package */ interface WebSocketClient { + + void open(); + + void close(); + + void send(String message); + + State getState(); + + interface WebSocketClientCallback { + void onOpen(); + + void onMessage(String message); + + void onClose(); + + void onError(Throwable exception); + + void stateChanged(); + } + + enum State {NONE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED} + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClientFactory.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClientFactory.java new file mode 100644 index 0000000..1a98ed1 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/main/java/com/parse/WebSocketClientFactory.java @@ -0,0 +1,9 @@ +package com.parse; + +import java.net.URI; + +/* package */ interface WebSocketClientFactory { + + WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl); + +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/ImmediateExecutor.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/ImmediateExecutor.java new file mode 100644 index 0000000..4fa556f --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/ImmediateExecutor.java @@ -0,0 +1,10 @@ +package com.parse; + +import java.util.concurrent.Executor; + +class ImmediateExecutor implements Executor { + @Override + public void execute(Runnable runnable) { + runnable.run(); + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/TestParseLiveQueryClient.java b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/TestParseLiveQueryClient.java new file mode 100644 index 0000000..ec2c5ca --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery/src/test/java/com/parse/TestParseLiveQueryClient.java @@ -0,0 +1,609 @@ +package com.parse; + +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.util.Transcript; + +import java.io.IOException; +import java.net.URI; + +import bolts.Task; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.and; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import com.parse.livequery.BuildConfig; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21) +public class TestParseLiveQueryClient { + + private WebSocketClient webSocketClient; + private WebSocketClient.WebSocketClientCallback webSocketClientCallback; + private ParseLiveQueryClient parseLiveQueryClient; + + private ParseUser mockUser; + + @Before + public void setUp() throws Exception { + ParsePlugins.initialize("1234", "1234"); + + // Register a mock currentUserController to make getCurrentUser work + mockUser = mock(ParseUser.class); + ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class); + when(currentUserController.getAsync(anyBoolean())).thenAnswer(new Answer>() { + @Override + public Task answer(InvocationOnMock invocation) throws Throwable { + return Task.forResult(mockUser); + } + }); + when(currentUserController.getCurrentSessionTokenAsync()).thenAnswer(new Answer>() { + @Override + public Task answer(InvocationOnMock invocation) throws Throwable { + return Task.forResult(mockUser.getSessionToken()); + } + }); + ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController); + + parseLiveQueryClient = ParseLiveQueryClient.Factory.getClient(new URI(""), new WebSocketClientFactory() { + @Override + public WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl) { + TestParseLiveQueryClient.this.webSocketClientCallback = webSocketClientCallback; + webSocketClient = mock(WebSocketClient.class); + return webSocketClient; + } + }, new ImmediateExecutor()); + reconnect(); + } + + @After + public void tearDown() throws Exception { + ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + } + + @Test + public void testSubscribeAfterSocketConnectBeforeConnectedOp() throws Exception { + // Bug: https://github.com/parse-community/ParseLiveQuery-Android/issues/46 + ParseQuery queryA = ParseQuery.getQuery("objA"); + ParseQuery queryB = ParseQuery.getQuery("objB"); + clearConnection(); + + // This will trigger connectIfNeeded(), which calls reconnect() + SubscriptionHandling subA = parseLiveQueryClient.subscribe(queryA); + + verify(webSocketClient, times(1)).open(); + verify(webSocketClient, never()).send(anyString()); + + // Now the socket is open + webSocketClientCallback.onOpen(); + when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED); + // and we send op=connect + verify(webSocketClient, times(1)).send(contains("\"op\":\"connect\"")); + + // Now if we subscribe to queryB, we SHOULD NOT send the subscribe yet, until we get op=connected + SubscriptionHandling subB = parseLiveQueryClient.subscribe(queryB); + verify(webSocketClient, never()).send(contains("\"op\":\"subscribe\"")); + + // on op=connected, _then_ we should send both subscriptions + webSocketClientCallback.onMessage(createConnectedMessage().toString()); + verify(webSocketClient, times(2)).send(contains("\"op\":\"subscribe\"")); + } + + @Test + public void testSubscribeWhenSubscribedToCallback() throws Exception { + SubscriptionHandling.HandleSubscribeCallback subscribeMockCallback = mock(SubscriptionHandling.HandleSubscribeCallback.class); + + ParseQuery parseQuery = new ParseQuery<>("test"); + createSubscription(parseQuery, subscribeMockCallback); + + verify(subscribeMockCallback, times(1)).onSubscribe(parseQuery); + } + + @Test + public void testUnsubscribeWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + parseLiveQueryClient.unsubscribe(parseQuery); + verify(webSocketClient, times(1)).send(any(String.class)); + + SubscriptionHandling.HandleUnsubscribeCallback unsubscribeMockCallback = mock( + SubscriptionHandling.HandleUnsubscribeCallback.class); + subscriptionHandling.handleUnsubscribe(unsubscribeMockCallback); + webSocketClientCallback.onMessage(createUnsubscribedMessage(subscriptionHandling.getRequestId()).toString()); + + verify(unsubscribeMockCallback, times(1)).onUnsubscribe(parseQuery); + } + + @Test + public void testErrorWhileSubscribing() throws Exception { + ParseQuery.State state = mock(ParseQuery.State.class); + when(state.constraints()).thenThrow(new RuntimeException("forced error")); + + ParseQuery.State.Builder builder = mock(ParseQuery.State.Builder.class); + when(builder.build()).thenReturn(state); + ParseQuery query = mock(ParseQuery.class); + when(query.getBuilder()).thenReturn(builder); + + SubscriptionHandling handling = parseLiveQueryClient.subscribe(query); + + SubscriptionHandling.HandleErrorCallback errorMockCallback = mock(SubscriptionHandling.HandleErrorCallback.class); + handling.handleError(errorMockCallback); + + // Trigger a re-subscribe + webSocketClientCallback.onMessage(createConnectedMessage().toString()); + + // This will never get a chance to call op=subscribe, because an exception was thrown + verify(webSocketClient, never()).send(anyString()); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(LiveQueryException.class); + verify(errorMockCallback, times(1)).onError(eq(query), errorCaptor.capture()); + + assertEquals("Error when subscribing", errorCaptor.getValue().getMessage()); + assertNotNull(errorCaptor.getValue().getCause()); + } + + @Test + public void testErrorWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleErrorCallback errorMockCallback = mock(SubscriptionHandling.HandleErrorCallback.class); + subscriptionHandling.handleError(errorMockCallback); + webSocketClientCallback.onMessage(createErrorMessage(subscriptionHandling.getRequestId()).toString()); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(LiveQueryException.class); + verify(errorMockCallback, times(1)).onError(eq(parseQuery), errorCaptor.capture()); + + LiveQueryException genericError = errorCaptor.getValue(); + assertTrue(genericError instanceof LiveQueryException.ServerReportedException); + + LiveQueryException.ServerReportedException serverError = (LiveQueryException.ServerReportedException) genericError; + assertEquals(serverError.getError(), "testError"); + assertEquals(serverError.getCode(), 1); + assertEquals(serverError.isReconnect(), true); + } + + @Test + public void testHeterogeneousSubscriptions() throws Exception { + ParseObject.registerSubclass(MockClassA.class); + ParseObject.registerSubclass(MockClassB.class); + + ParseQuery query1 = ParseQuery.getQuery(MockClassA.class); + ParseQuery query2 = ParseQuery.getQuery(MockClassB.class); + SubscriptionHandling handle1 = parseLiveQueryClient.subscribe(query1); + SubscriptionHandling handle2 = parseLiveQueryClient.subscribe(query2); + + handle1.handleError(new SubscriptionHandling.HandleErrorCallback() { + @Override + public void onError(ParseQuery query, LiveQueryException exception) { + throw new RuntimeException(exception); + } + }); + handle2.handleError(new SubscriptionHandling.HandleErrorCallback() { + @Override + public void onError(ParseQuery query, LiveQueryException exception) { + throw new RuntimeException(exception); + } + }); + + SubscriptionHandling.HandleEventCallback eventMockCallback1 = mock(SubscriptionHandling.HandleEventCallback.class); + SubscriptionHandling.HandleEventCallback eventMockCallback2 = mock(SubscriptionHandling.HandleEventCallback.class); + + handle1.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback1); + handle2.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback2); + + ParseObject parseObject1 = new MockClassA(); + parseObject1.setObjectId("testId1"); + + ParseObject parseObject2 = new MockClassB(); + parseObject2.setObjectId("testId2"); + + webSocketClientCallback.onMessage(createObjectCreateMessage(handle1.getRequestId(), parseObject1).toString()); + webSocketClientCallback.onMessage(createObjectCreateMessage(handle2.getRequestId(), parseObject2).toString()); + + validateSameObject((SubscriptionHandling.HandleEventCallback) eventMockCallback1, (ParseQuery) query1, parseObject1); + validateSameObject((SubscriptionHandling.HandleEventCallback) eventMockCallback2, (ParseQuery) query2, parseObject2); + } + + @Test + public void testCreateEventWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + validateSameObject(eventMockCallback, parseQuery, parseObject); + } + + @Test + public void testEnterEventWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.ENTER, eventMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectEnterMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + validateSameObject(eventMockCallback, parseQuery, parseObject); + } + + @Test + public void testUpdateEventWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.UPDATE, eventMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectUpdateMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + validateSameObject(eventMockCallback, parseQuery, parseObject); + } + + @Test + public void testLeaveEventWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.LEAVE, eventMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectLeaveMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + validateSameObject(eventMockCallback, parseQuery, parseObject); + } + + @Test + public void testDeleteEventWhenSubscribedToCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.DELETE, eventMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectDeleteMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + validateSameObject(eventMockCallback, parseQuery, parseObject); + } + + @Test + public void testCreateEventWhenSubscribedToAnyCallback() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventsCallback eventsMockCallback = mock(SubscriptionHandling.HandleEventsCallback.class); + subscriptionHandling.handleEvents(eventsMockCallback); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + + webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(ParseObject.class); + verify(eventsMockCallback, times(1)).onEvents(eq(parseQuery), eq(SubscriptionHandling.Event.CREATE), objectCaptor.capture()); + + ParseObject newParseObject = objectCaptor.getValue(); + + assertEquals(parseObject.getObjectId(), newParseObject.getObjectId()); + } + + @Test + public void testSubscriptionStoppedAfterUnsubscribe() throws Exception { + ParseQuery parseQuery = new ParseQuery<>("test"); + SubscriptionHandling subscriptionHandling = createSubscription(parseQuery, + mock(SubscriptionHandling.HandleSubscribeCallback.class)); + + SubscriptionHandling.HandleEventCallback eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class); + subscriptionHandling.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback); + + SubscriptionHandling.HandleUnsubscribeCallback unsubscribeMockCallback = mock( + SubscriptionHandling.HandleUnsubscribeCallback.class); + subscriptionHandling.handleUnsubscribe(unsubscribeMockCallback); + + parseLiveQueryClient.unsubscribe(parseQuery); + verify(webSocketClient, times(1)).send(any(String.class)); + webSocketClientCallback.onMessage(createUnsubscribedMessage(subscriptionHandling.getRequestId()).toString()); + verify(unsubscribeMockCallback, times(1)).onUnsubscribe(parseQuery); + + ParseObject parseObject = new ParseObject("Test"); + parseObject.setObjectId("testId"); + webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString()); + + ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(ParseObject.class); + verify(eventMockCallback, times(0)).onEvent(eq(parseQuery), objectCaptor.capture()); + } + + @Test + public void testSubscriptionReplayedAfterReconnect() throws Exception { + SubscriptionHandling.HandleSubscribeCallback subscribeMockCallback = mock(SubscriptionHandling.HandleSubscribeCallback.class); + + ParseQuery parseQuery = new ParseQuery<>("test"); + createSubscription(parseQuery, subscribeMockCallback); + + parseLiveQueryClient.disconnect(); + reconnect(); + + verify(webSocketClient, times(2)).send(any(String.class)); + } + + @Test + public void testSessionTokenSentOnConnect() { + when(mockUser.getSessionToken()).thenReturn("the token"); + parseLiveQueryClient.reconnect(); + webSocketClientCallback.onOpen(); + verify(webSocketClient, times(1)).send(contains("\"sessionToken\":\"the token\"")); + } + + @Test + public void testEmptySessionTokenOnConnect() { + parseLiveQueryClient.reconnect(); + webSocketClientCallback.onOpen(); + verify(webSocketClient, times(1)).send(not(contains("\"sessionToken\":"))); + } + + @Test + public void testSessionTokenSentOnSubscribe() { + when(mockUser.getSessionToken()).thenReturn("the token"); + when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED); + parseLiveQueryClient.subscribe(ParseQuery.getQuery("Test")); + verify(webSocketClient, times(1)).send(and( + contains("\"op\":\"subscribe\""), + contains("\"sessionToken\":\"the token\""))); + } + + @Test + public void testEmptySessionTokenOnSubscribe() { + when(mockUser.getSessionToken()).thenReturn("the token"); + when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED); + parseLiveQueryClient.subscribe(ParseQuery.getQuery("Test")); + verify(webSocketClient, times(1)).send(contains("\"op\":\"connect\"")); + verify(webSocketClient, times(1)).send(and( + contains("\"op\":\"subscribe\""), + contains("\"sessionToken\":\"the token\""))); + } + + @Test + public void testCallbackNotifiedOnUnexpectedDisconnect() throws Exception { + LoggingCallbacks callbacks = new LoggingCallbacks(); + parseLiveQueryClient.registerListener(callbacks); + callbacks.transcript.assertNoEventsSoFar(); + + // Unexpected close from the server: + webSocketClientCallback.onClose(); + callbacks.transcript.assertEventsSoFar("onLiveQueryClientDisconnected: false"); + } + + @Test + public void testCallbackNotifiedOnExpectedDisconnect() throws Exception { + LoggingCallbacks callbacks = new LoggingCallbacks(); + parseLiveQueryClient.registerListener(callbacks); + callbacks.transcript.assertNoEventsSoFar(); + + parseLiveQueryClient.disconnect(); + verify(webSocketClient, times(1)).close(); + + callbacks.transcript.assertNoEventsSoFar(); + // the client is a mock, so it won't actually invoke the callback automatically + webSocketClientCallback.onClose(); + callbacks.transcript.assertEventsSoFar("onLiveQueryClientDisconnected: true"); + } + + @Test + public void testCallbackNotifiedOnConnect() throws Exception { + LoggingCallbacks callbacks = new LoggingCallbacks(); + parseLiveQueryClient.registerListener(callbacks); + callbacks.transcript.assertNoEventsSoFar(); + + reconnect(); + callbacks.transcript.assertEventsSoFar("onLiveQueryClientConnected"); + } + + @Test + public void testCallbackNotifiedOnSocketError() throws Exception { + LoggingCallbacks callbacks = new LoggingCallbacks(); + parseLiveQueryClient.registerListener(callbacks); + callbacks.transcript.assertNoEventsSoFar(); + + webSocketClientCallback.onError(new IOException("bad things happened")); + callbacks.transcript.assertEventsSoFar("onSocketError: java.io.IOException: bad things happened", + "onLiveQueryClientDisconnected: false"); + } + + @Test + public void testCallbackNotifiedOnServerError() throws Exception { + LoggingCallbacks callbacks = new LoggingCallbacks(); + parseLiveQueryClient.registerListener(callbacks); + callbacks.transcript.assertNoEventsSoFar(); + + webSocketClientCallback.onMessage(createErrorMessage(1).toString()); + callbacks.transcript.assertEventsSoFar("onLiveQueryError: com.parse.LiveQueryException$ServerReportedException: Server reported error; code: 1, error: testError, reconnect: true"); + } + + private SubscriptionHandling createSubscription(ParseQuery parseQuery, + SubscriptionHandling.HandleSubscribeCallback subscribeMockCallback) throws Exception { + SubscriptionHandling subscriptionHandling = parseLiveQueryClient.subscribe(parseQuery).handleSubscribe(subscribeMockCallback); + webSocketClientCallback.onMessage(createSubscribedMessage(subscriptionHandling.getRequestId()).toString()); + return subscriptionHandling; + } + + private void validateSameObject(SubscriptionHandling.HandleEventCallback eventMockCallback, + ParseQuery parseQuery, + ParseObject originalParseObject) { + ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(ParseObject.class); + verify(eventMockCallback, times(1)).onEvent(eq(parseQuery), objectCaptor.capture()); + + ParseObject newParseObject = objectCaptor.getValue(); + + assertEquals(originalParseObject.getClassName(), newParseObject.getClassName()); + assertEquals(originalParseObject.getObjectId(), newParseObject.getObjectId()); + } + + private void clearConnection() { + webSocketClient = null; + webSocketClientCallback = null; + } + + private void reconnect() { + parseLiveQueryClient.reconnect(); + webSocketClientCallback.onOpen(); + try { + webSocketClientCallback.onMessage(createConnectedMessage().toString()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + private static JSONObject createConnectedMessage() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "connected"); + return jsonObject; + } + + private static JSONObject createSubscribedMessage(int requestId) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "subscribed"); + jsonObject.put("clientId", 1); + jsonObject.put("requestId", requestId); + return jsonObject; + } + + private static JSONObject createUnsubscribedMessage(int requestId) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "unsubscribed"); + jsonObject.put("requestId", requestId); + return jsonObject; + } + + private static JSONObject createErrorMessage(int requestId) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "error"); + jsonObject.put("requestId", requestId); + jsonObject.put("code", 1); + jsonObject.put("error", "testError"); + jsonObject.put("reconnect", true); + return jsonObject; + } + + private static JSONObject createObjectCreateMessage(int requestId, ParseObject parseObject) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "create"); + jsonObject.put("requestId", requestId); + jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject)); + return jsonObject; + } + + private static JSONObject createObjectEnterMessage(int requestId, ParseObject parseObject) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "enter"); + jsonObject.put("requestId", requestId); + jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject)); + return jsonObject; + } + + private static JSONObject createObjectUpdateMessage(int requestId, ParseObject parseObject) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "update"); + jsonObject.put("requestId", requestId); + jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject)); + return jsonObject; + } + + private static JSONObject createObjectLeaveMessage(int requestId, ParseObject parseObject) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "leave"); + jsonObject.put("requestId", requestId); + jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject)); + return jsonObject; + } + + private static JSONObject createObjectDeleteMessage(int requestId, ParseObject parseObject) throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("op", "delete"); + jsonObject.put("requestId", requestId); + jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject)); + return jsonObject; + } + + private static class LoggingCallbacks implements ParseLiveQueryClientCallbacks { + final Transcript transcript = new Transcript(); + + @Override + public void onLiveQueryClientConnected(ParseLiveQueryClient client) { + transcript.add("onLiveQueryClientConnected"); + } + + @Override + public void onLiveQueryClientDisconnected(ParseLiveQueryClient client, boolean userInitiated) { + transcript.add("onLiveQueryClientDisconnected: " + userInitiated); + } + + @Override + public void onLiveQueryError(ParseLiveQueryClient client, LiveQueryException reason) { + transcript.add("onLiveQueryError: " + reason); + } + + @Override + public void onSocketError(ParseLiveQueryClient client, Throwable reason) { + transcript.add("onSocketError: " + reason); + } + } + + @ParseClassName("MockA") + static class MockClassA extends ParseObject { + } + + @ParseClassName("MockB") + static class MockClassB extends ParseObject { + } +} diff --git a/ExternalLibs/ParseLiveQuery-Android/README.md b/ExternalLibs/ParseLiveQuery-Android/README.md new file mode 100644 index 0000000..ee9a153 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/README.md @@ -0,0 +1,131 @@ +# Parse LiveQuery Client for Android +[![Build Status][build-status-svg]][build-status-link] +[![Coverage Status][coverage-status-svg]][coverage-status-link] +[![Maven Central][maven-svg]][maven-link] +[![License][license-svg]][license-link] + +`ParseQuery` is one of the key concepts for Parse. It allows you to retrieve `ParseObject`s by specifying some conditions, making it easy to build apps such as a dashboard, a todo list or even some strategy games. However, `ParseQuery` is based on a pull model, which is not suitable for apps that need real-time support. + +Suppose you are building an app that allows multiple users to edit the same file at the same time. `ParseQuery` would not be an ideal tool since you can not know when to query from the server to get the updates. + +To solve this problem, we introduce Parse LiveQuery. This tool allows you to subscribe to a `ParseQuery` you are interested in. Once subscribed, the server will notify clients whenever a `ParseObject` that matches the `ParseQuery` is created or updated, in real-time. + +## Setup Server + +Parse LiveQuery contains two parts, the LiveQuery server and the LiveQuery clients. In order to use live queries, you need to set up both of them. + +The easiest way to setup the LiveQuery server is to make it run with the [Open Source Parse Server](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery#server-setup). + +## Setup Client +Download [the latest JAR][latest] or define in Gradle: + +```groovy +dependencies { + compile 'com.parse:parse-livequery-android:1.0.4' +} +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. + +## Use Client + + +The LiveQuery client interface is based around the concept of `Subscriptions`. You can register any `ParseQuery` for live updates from the associated live query server, by simply calling `subscribe()` on the client: +```java +// Parse.initialize should be called first + +ParseLiveQueryClient parseLiveQueryClient = ParseLiveQueryClient.Factory.getClient(); +``` + +### Creating Live Queries + +Live querying depends on creating a subscription to a `ParseQuery`: + +```java +ParseQuery parseQuery = ParseQuery.getQuery(Message.class); + +SubscriptionHandling subscriptionHandling = parseLiveQueryClient.subscribe(parseQuery) +``` + +Once you've subscribed to a query, you can `handle` events on them, like so: +```java +subscriptionHandling.handleEvents(new SubscriptionHandling.HandleEventsCallback() { + @Override + public void onEvents(ParseQuery query, SubscriptionHandling.Event event, ParseObject object) { + // HANDLING all events + } +}) +``` + +You can also handle a single type of event, if that's all you're interested in: +```java +subscriptionHandling.handleEvent(SubscriptionHandling.Event.CREATE, new SubscriptionHandling.HandleEventCallback() { + @Override + public void onEvent(ParseQuery query, ParseObject object) { + // HANDLING create event + } +}) +``` + +Handling errors is and other events is similar, take a look at the `SubscriptionHandling` class for more information. + +## Advanced Usage + +If you wish to pass in your own OkHttpClient instance for troubleshooting or custom configs, you can instantiate the client as follows: + +```java +ParseLiveQueryClient parseLiveQueryClient = ParseLiveQueryClient.Factory.getClient(new OkHttp3SocketClientFactory(new OkHttpClient())); +``` + +The URL is determined by the Parse initialization, but you can override by specifying a `URI` object: + +```java +ParseLiveQueryClient parseLiveQueryClient = ParseLiveQueryClient.Factory.getClient(new URI("wss://myparseinstance.com")); +``` + +Note: The expected protocol for URI is `ws` instead of `http`, like in this example: `URI("ws://192.168.0.1:1337/1")`. + +## Build commands +Everything can done through the supplied gradle wrapper: + +### Compile a JAR +``` +./gradlew clean jarRelease +``` +Outputs can be found in `ParseLiveQuery/build/libs/` + +### Run the Tests +``` +./gradlew clean testDebug +``` +Results can be found in `ParseLiveQuery/build/reports/` + +### Get Code Coverage Reports +``` +./gradlew clean jacocoTestReport +``` +Results can be found in `ParseLiveQuery/build/reports/` + +## How Do I Contribute? +We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines](CONTRIBUTING.md). + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. + + [parse.com]: https://www.parse.com/products/android + [guide]: https://www.parse.com/docs/android/guide + [blog]: https://blog.parse.com/ + + [latest]: https://search.maven.org/remote_content?g=com.parse&a=parse-livequery-android&v=LATEST + [snap]: https://oss.sonatype.org/content/repositories/snapshots/ + + [build-status-svg]: https://img.shields.io/travis/parse-community/ParseLiveQuery-Android/master.svg + [build-status-link]: https://travis-ci.org/parse-community/ParseLiveQuery-Android/branches + [coverage-status-svg]: https://img.shields.io/codecov/c/github/parse-community/ParseLiveQuery-Android/master.svg + [coverage-status-link]: https://codecov.io/github/parse-community/ParseLiveQuery-Android?branch=master + [maven-svg]: https://maven-badges.herokuapp.com/maven-central/com.parse/parse-livequery-android/badge.svg?style=flat + [maven-link]: https://maven-badges.herokuapp.com/maven-central/com.parse/parse-livequery-android + + [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg + [license-link]: https://github.com/parse-community/ParseLiveQuery-Android/blob/master/LICENSE diff --git a/ExternalLibs/ParseLiveQuery-Android/THIRD_PARTY_NOTICES.txt b/ExternalLibs/ParseLiveQuery-Android/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..ed8e847 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,218 @@ +This project contains portions of third party software provided under the following terms: + +Apache Commons IO +Copyright 2002-2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Files from this library have been modified. Parse provides its modifications under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + You must give any other recipients of the Work or Derivative Works a copy of this License; and + You must cause any modified files to carry prominent notices stating that You changed the files; and + You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +----- + +Guava + +/* + * Copyright (C) 2009 The Guava Authors + * + * 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. + */ + +Files from this library have been modified. Parse provides its modifications under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + You must give any other recipients of the Work or Derivative Works a copy of this License; and + You must cause any modified files to carry prominent notices stating that You changed the files; and + You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +----- + +Android Open Source Project + +/* + * 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. + */ + +Files from this library have been modified. Parse provides its modifications under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + You must give any other recipients of the Work or Derivative Works a copy of this License; and + You must cause any modified files to carry prominent notices stating that You changed the files; and + You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/ExternalLibs/ParseLiveQuery-Android/build.gradle b/ExternalLibs/ParseLiveQuery-Android/build.gradle new file mode 100644 index 0000000..3c1b22c --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.0' + } +} + +plugins { + id "com.jfrog.bintray" version "1.7.3" +} + +allprojects { + repositories { + jcenter() + } +} + +ext { + compileSdkVersion = 25 + buildToolsVersion = "25.0.2" + + minSdkVersion = 9 + targetSdkVersion = 25 +} diff --git a/ExternalLibs/ParseLiveQuery-Android/gradle/ExcludeDoclet.jar b/ExternalLibs/ParseLiveQuery-Android/gradle/ExcludeDoclet.jar new file mode 100644 index 0000000000000000000000000000000000000000..0fb5f89dc52371d8fbcdb0ff58c32dd781233fe0 GIT binary patch literal 3539 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 o1_w0`8j?RXMrrt6cqqKIJAHI7g&)0;{X5v literal 0 HcmV?d00001 diff --git a/ExternalLibs/ParseLiveQuery-Android/gradle/wrapper/gradle-wrapper.jar b/ExternalLibs/ParseLiveQuery-Android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..667288ad6c2b3b87c990ece1267e56f0bcbf3622 GIT binary patch literal 50514 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/ExternalLibs/ParseLiveQuery-Android/gradlew.bat b/ExternalLibs/ParseLiveQuery-Android/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ExternalLibs/ParseLiveQuery-Android/scripts/publish_snapshot.sh b/ExternalLibs/ParseLiveQuery-Android/scripts/publish_snapshot.sh new file mode 100755 index 0000000..38abf0a --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/scripts/publish_snapshot.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Publishes SNAPSHOTs + +REPO_SLUG=parse-community/ParseLiveQuery-Android +BRANCH=master + +set -e + +if [ "$TRAVIS_REPO_SLUG" != "$REPO_SLUG" ]; then + echo "Skipping publishing SNAPSHOT: wrong repository. Expected '$REPO_SLUG' but was '$TRAVIS_REPO_SLUG'" +elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo "Skipping publishing SNAPSHOT: was PR" +elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then + echo "Skipping publishing SNAPSHOT: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'" +else + echo "Publishing SNAPSHOT..." + ./gradlew uploadArchives + echo "SNAPSHOT published!" +fi diff --git a/ExternalLibs/ParseLiveQuery-Android/settings.gradle b/ExternalLibs/ParseLiveQuery-Android/settings.gradle new file mode 100644 index 0000000..467a08b --- /dev/null +++ b/ExternalLibs/ParseLiveQuery-Android/settings.gradle @@ -0,0 +1 @@ +include ':ParseLiveQuery' diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d2aea17 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 26 + defaultConfig { + applicationId "net.adphi.apps.parseapplication" + minSdkVersion 16 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + implementation project(':Parse') + implementation project(':ParseLiveQuery') +} + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/net/adphi/apps/parseapplication/ExampleInstrumentedTest.kt b/app/src/androidTest/java/net/adphi/apps/parseapplication/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..4bc1751 --- /dev/null +++ b/app/src/androidTest/java/net/adphi/apps/parseapplication/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package net.adphi.apps.parseapplication + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("net.adphi.apps.parseapplication", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..06e7136 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/adphi/apps/parseapplication/MainActivity.kt b/app/src/main/java/net/adphi/apps/parseapplication/MainActivity.kt new file mode 100644 index 0000000..f9ced7e --- /dev/null +++ b/app/src/main/java/net/adphi/apps/parseapplication/MainActivity.kt @@ -0,0 +1,15 @@ +package net.adphi.apps.parseapplication + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import net.adphi.apps.parseapplication.Utils.getTAG + +class MainActivity : AppCompatActivity() { + + private val TAG: String = getTAG(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/app/src/main/java/net/adphi/apps/parseapplication/ParseApplication.kt b/app/src/main/java/net/adphi/apps/parseapplication/ParseApplication.kt new file mode 100644 index 0000000..fbaa0f0 --- /dev/null +++ b/app/src/main/java/net/adphi/apps/parseapplication/ParseApplication.kt @@ -0,0 +1,29 @@ +package net.adphi.apps.parseapplication + +import android.app.Application +import com.parse.Parse +import com.parse.ParseLiveQueryClient +import net.adphi.apps.parseapplication.Utils.getTAG +import net.adphi.apps.parseapplication.Utils.registerParseObject +import java.net.URI + + +/** + * Created by Philippe-Adrien on 25/03/2018. + */ + +@Suppress("UNUSED") +class ParseApplication : Application() { + + private val TAG: String = getTAG(this) + + override fun onCreate() { + super.onCreate() + + registerParseObject() + val config: Parse.Configuration = Parse.Configuration.Builder(this) + .maxRetries(0).build() + Parse.initialize(config) + val liveQueryClient = ParseLiveQueryClient.Factory.getClient(URI("wss://parsetest.back4app.io")) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/adphi/apps/parseapplication/Utils/ParseUtils.kt b/app/src/main/java/net/adphi/apps/parseapplication/Utils/ParseUtils.kt new file mode 100644 index 0000000..b0c2f18 --- /dev/null +++ b/app/src/main/java/net/adphi/apps/parseapplication/Utils/ParseUtils.kt @@ -0,0 +1,9 @@ +package net.adphi.apps.parseapplication.Utils + +/** + * Created by Philippe-Adrien on 25/03/2018. + */ + +fun registerParseObject() : Unit { + +} \ No newline at end of file diff --git a/app/src/main/java/net/adphi/apps/parseapplication/Utils/Utils.kt b/app/src/main/java/net/adphi/apps/parseapplication/Utils/Utils.kt new file mode 100644 index 0000000..f11657d --- /dev/null +++ b/app/src/main/java/net/adphi/apps/parseapplication/Utils/Utils.kt @@ -0,0 +1,9 @@ +package net.adphi.apps.parseapplication.Utils + +/** + * Created by Philippe-Adrien on 25/03/2018. + */ + +fun getTAG(clazz: Any) : String { + return "ParseApplication ${clazz::class.java.simpleName}" +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c3903ed --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5713f34 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..e1db4cd --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 zcmZu&byQSev_3Py&@gnDfPjP`DLFJqiULXtibx~fLnvK>bPOP+(%nO&(%r2fA>H-( zz4z~1>*iYL?tRWZ_k8=?-?=ADTT_`3j}{LAK&YyspmTRd|F`47?v6Thw%7njTB|C^ zKKGc}$-p)u@1g1$=G5ziQhGf`pecnFHQK@{)H)R`NQF;K%92o17K-93yUfN21$b29 zQwz1oFs@r6GO|&!sP_4*_5J}y@1EmX38MLHp9O5Oe0Nc6{^^wzO4l(d z;mtZ_YZu`gPyE@_DZic*_^gGkxh<(}XliiFNpj1&`$dYO3scX$PHr^OPt}D-`w9aR z4}a$o1nmaz>bV)|i2j5($CXJ<=V0%{^_5JXJ2~-Q=5u(R41}kRaj^33P50Hg*ot1f z?w;RDqu}t{QQ%88FhO3t>0-Sy@ck7!K1c53XC+HJeY@B0BH+W}BTA1!ueRG49Clr? z+R!2Jlc`n)zZ?XWaZO0BnqvRN#k{$*;dYA4UO&o_-b>h3>@8fgSjOUsv0wVwlxy0h z{E1|}P_3K!kMbGZt_qQIF~jd+Km4P8D0dwO{+jQ1;}@_Weti;`V}a_?BkaNJA?PXD zNGH$uRwng<4o9{nk4gW z3E-`-*MB=(J%0*&SA1UclA>pLfP4H?eSsQV$G$t!uXTEio7TY9E35&?0M-ERfX4he z{_Hb&AE`T%j8hIZEp@yBVycpvW2!bHrfxbuu6>_i<^9@?ak)9gHU*#bS~}$sGY*Fi z=%P&i3aH%N`b;I~s8{&6uGo$>-`ukQ<8ri(6aH6p_F`Fhdi6HuacwfQn10HVL7Om1 z4aZpjatkbgjp$L5Mceab#G#C)Hr{^W|TJX~?B3@2buj0;kfuNTf4c3*Au~O^aj=W2$j^4okeCxh#lwexN@eam-u4dNz zN2NIuIM4566{T&^k%4ftShcPk#=im-zXm>QWqH^0>A@?MqlDZCZ@8Wi*@tvhn5p<} zRwFm@gz|WZp91S5Z{}tB^e9|FBg(~Ik+?&_53J6ye_QQOSJ*846~H%s#LD}|O9v9H z1fLrrgoPo_&bs}eqEr}2en3iqAcP^>YsKiez$5-6m6(#3ZZ$@M5Ck=_Vv`QA>1A*v z3w-nJ_;5Nc(0_%`kG91#sotIlhO!*5#|yg+Gx{V;0ty`*=Y9=jCh$l*=fE(~t}%R# zc}iNpO)OZX`P=leQY^?^DF1w%FJh>Dkp}-o5Ig|2!6^E>|W|zc~W7gF;MtxX7 zV~UjQNsUC$EYXpN?~o{83D2c*0~7;Tm~%FRTAnnt3ln{?DcLZ=NsBY|JxwUA-6K3V zP&#|9t#a}Q4{Sg{6v-OmjJBkCh>m)8vLNm4lStMUT$)FZeJG05A)px&o3H)5oAl9= z31@?HyCriHcCDnt628BFN+T;U69Wl#itfvqIDBydMvOJO0Zl?go$cfG5>TK75CMj3 zakLaH3=&J0e}Xmqlav$S0>E@_Yo_V~3SiiXrw)$&!XhrHCDQ%P1BHPusuKr0LthAB zg)mDrLy>2*yevMMOQe6fZ|)%PEb!lC^*9yaX9UMy7-v!fSICssTR|wML0Ic2BhKAq z3I1X~ z7^_!M&;6Z9?br3#HU_&kfJ~%botXQkC1v<}ZZxN5q-T)|Sb2cW3WYUBbDZ`TH{!*^ zrmAeRM+(QI>D+?}guZ+dH*X)@^!O|oL69&Avbtw2^M3HP(+2kV{O$^3BN1RLfrC8nwz7=VhBR%>!;7WR<~;34B_j3A{>^@e@H+Q! zL=UNr1(JvKAQLKT0b}EMn|QUWtY>!>8-t@fVj_&`~gGd{_aPy5W>0u5L$zrsU^rBO=i$`#Xd*>kh)lPf}A znNXSEl`+HlhXtylgS9(#N02A=zVV?#OF?)Gr>(HszVa+1*2VG@qYttJuXaBlzP`Pb zX)ueu?s&}R>xI#^*r4gR?tMFi!_eeKlIM5g)Nk)Y^h=ZCR**xY>$E5knctRrq!zw? zX{2|hwR9LXTY1)pTlKg7U4_ej{dcj2{!+1sZ6<@9^?mn)=37V)DIAvS(}S`IgFO!6 zn({?nYw`Z-@jvt@!q|5z?TI3(dx^1szSn%azAwp>N#fk^kt|=MejKtacAs@Rdku#zT>9$s z=m7ek)`=O7hO2n+2Uj$QUs&2EIqycF{(L9Y#^IyxXA%R@ z&j`VAprIV~d!pH-7~zA+bjwVn3kOB3;rlg{nr&wHV12N}g^i>Upls~=z`VX>9HQ#= zTu&luVb@_Lkz63&&^_M!6(-2^0?GCAX9XKp{O={pd|AlIMGriX6s_Jy8_q9|{5jLc zxd1aj_ucE7Vcti#$r!s~w~W=XpaLQ}#mX`apR7^n9-d3?O+adJYr*L;{c)x@REewM@vZN0njS3iE$88KHPWAkWt((OUMherUnPm?i&8@!9E@ zUW^$%CpdruZR0ohzUq-XQ$KEIB8Sjgs1+wKSUH&Y;=ee%E&O$X18{&979d~K2uJW` zd*8awHCXb;Q>4z$B|sPNv+Zd__f6&@KmS+L`z3H1x+x|Xs7-N-iw|1C=QiJdU)f~z z{vO4hpP`0MyqmwIHN=l?jSq>OKG6CEC#O`*blP`?>)CUWj5j1cB>%6N7;`kfZ1iQV zam~SDB?{uyp^=vF_u|=8xn3S)L;wF8ZRZV{bezM-EH;MC91JQZ{KcZZ$IWJUy?SJGeGUWm6PeuO8-K2|hD~p;Ls~9Y-4lE+?|bF)XaNKUNX(K7 zBQk0Z{n>hrH-CA`bTr$6z0n@Cn9EL$XZ3=X7NopjcI=;z<(X7-oEmK}BId=PxX*!b7Q6oL@ufd%eEPc`_la(}WkT zKe?-YJWn^6b$^{dhdJZ)I!Kn6c}iw%o5mLDyvM7qJZbkGG?zLU;M|W;Wis|A;SuY3{_X53`+>9g^B%O4b{;^t$^;{oKHbo*CY%u91 zp#2d8Pg=I0&UX{qwr=y=o_^BLdk=KYH$=Z8+k|p8V5`ph~3b^{^NnL4m_+4zx( zeoTt@f<$DmsB1}o%R1Hx`ToPuBl+P6cb-?uF{1!z-2WvdR4+vJ*SYTic5@gwnzu%e zD!HF^X=$ha^#1hi*@~^nDL!HQ;MC&e+6=onaJgm-J-+|>PpmU=SIe?EQE5vJiqziw z*K=Z%bWZz_we!qiFqE`I?#$yozNxIE7Ei;csv>++r*?)0bozFpF&oLh94u z-2c2L`5BarP7l>87|f)vxaT*9(!Q`2xBMZ&^JVj-|1)Tg!6OW=lk=w zLwVlr!*<(l*L$a?ox3+%!~UIj3Ej@KD;W>1E_c)1szDi93BC;0K?drOQ>@$yi|DtT zSir}!Yx>znf&b0KS;Lk7VKPDF@e>(qQr0%SNcGQd(p9StjqJ`QSW&c{ggF?5{d22w zlkX%JTUq`;(3WSH+)WHl%qlF)iNG_?}K?ZM3cS7#u5v zZ!apx4Apv=PWsn}eD%MI#=KA)OlNy0)l@~D^1;NC5k@|OPW3wt>WNYDN+8~+gM%E! z$ z`Olr0;eytiK&~O*ps%KV?2vq+DhuRh*!6Ilzu>A;iMe9 zI?zug9nT9CI_o)O}KF_I_U z_Cswu{)3pCYgw{eOt#E?UCqBwkAugSl>5 zX?G=Ci(Lo+r3suuJezyQyDvw*<1b{rx*&ZaY2HlJ>k{Qc%IZeU43pQXw4mh!4I5>l zZ@4$uxaPY#!*IhL4Hctn#!n#S+SiPcZP_PTd5fXf1exhFi5zf3kl`UcW2RUk)F2oF z_ogN`{03PiseQR;fa#{Uy;jeNlJ0Sle`~;ZYhLjkuy>a^!Z_nR~`$&F?NVuIE3HX;i zD82snwlwPb`7yE)ZA_Ndmq5zuSO1{{1}(d9u4#!Fl_|eOuxKBwOfQ*tG`VjCV$-WF zxi0c&+w}Z)rqz{%f46@`ADPdGm#x)+zpT+gyfDi;_P zR{#Ta`Mzd=putKO@5lQJO*aNy(i?}Ltwy^Z;69f|eqi#UCI1$vL!+(#mi?dK`OL$! z3jQnx$_$+Li2<__CL@Wuk4^J7-!n3j2I4N8e#=qpir+iEQcrn3`B4yNOd1BBLEni<(tdRWE>m0I^ zt(^*Td+S3}$5rOzXy=MW>%#MN_qy%5St!>HrGZ~Fq1WKw-&kv@2TrCcPCPzY%2aO- zN?7@+$4?&qA|uv{QHuV)O9haZpG7Jx2f%D)7J@oWTxJ#E_YSq_6qT1tomOD?02(1otT{Hk8{?g(944>h4f% zOJ8tzjecV{x2uWde&6oAP)*({ zFkW0Q%gdI*9@W)oKO65DgP<3F_BIKvRXLAR?Z61&0g2TR6mEZ7OZK?dP7zukdg?s_tNZeuOsh^e1Tmdlz5rIg?LcK|%aQ1FsSDv#W0EnHd z9M)p;gAL_R~Z5cojTdwy+qDsd6R01Vtxmq&FhfPz{wxmB$${zW~z@{Ro_ zK#y5^KqIp!#@or>GD`c+aZ(PV1=`Eo1?a55p6a*WepFgxvmp!^2518YEU-;{F}fLr zD~)=S0m=+px3TUN8-El}Xb}{2ET*_i3-|WlY@V7vr6#&cOr*+oS9?GF?@)K6op>>o z4af0@%KwaLr`{3P&)474<3rDMsd!IM-bepWfhfuMmJt}#0%PgDSx*q(s0m%ZFgWTj zwwvH%2!(i9{RHX~FVUB5qHvF{+ZF}+(bZVPG1)a*Ph>KV;cYNK^aB@R#dS~&`^60V zn2Z24Y{{djzK33}t@q%!v5k)u7jAXB_H{#4Ut2 z1}0j5$RXcTyfazqL9=^Qe%GL`G)=!lirv7AgVRf^=XyEM&kiOe_%JD!O?sXK&hrDo zF}m9B68im!oGshuZluy2H#T$`XPZQu@zf;(nBCZB-cjQ&w*p@Tm_$pe^MTN3EauI) zJG&G^H-4S|1OCd#@A6jO+IcAXG#5M-d9E!^YNmV7Z(=F^?8bfrYf&mLMnRd_22&Q} z2*msbLsrI!XPeOK@|V?n>`kNC`8eSFmekELLr|!-wQRltxZnuRedup<7VflowJ+gC z)F}P6lUSsh^B41?=~0*68YA6z63lKG`W$@{GV!cC2FCl0s<7yz6!3JWoBbUDTgpg% z4VNUk%xblMy7PjLF2We*3XY7K*N(*9Yx!_M zjU$&JXLiNxaTzoa&k@NSbzbLJTn$6bu6SPWYx)Zc1Li~Lqj($GuWsA#;zg85eH{yx zz3IIOea3A4QFGmJCfn7N_d$8a77j+T^W}Sr%0XdVLFf&zJ$s^D5Vrc!iV&GXyb5*A z6mG8d*6EDN7a;=dgVjYI--~4@Fe{{fcJ4B|;_Qg~&%6#?I(?X_$S4rDw{=>=8iZS=M^I#EF!m zXn%K_xXWwmm7R40LKXPo6ZzNZfN1-$S6RuVU=JlC|3#Xjo-%ebJvvC4n%IM)Q8NDh zGXd)L;ay_JMozc^mU*Uifnp=#+if>LD*O9MV#@wB1l``z|tlu(7PJqS6rm)0@ zJzP50{0Vpa`_?92oB;*i(?i225a6tZgT+9Dg?vTh)N4OKA~(c8{$8-ZKz=mb@$4IT9g8>;k11WIT+Y=%Z})`y#OJ zK-~rlEy!T%0h!Qo+jjPF2RQz2Z^B;dbvYg2JS`+@D~OWH{2-EEs^BdnuJskh>CKeT z1b;%8dU6QU%i@z?^6Q-{XESe^qRiw`ka+k!d-{c%&lXM}vCX^T=|?|;t6r?N*h-W4 z?o4Hy%BWqW+5=+md#5^8|49zjM zon_Do@rhzZ4XAb}-m|bMH$Vg<;^Bo6A8cfhUQ>|wFk~j(`>1NgD3sTg)He1pWrUj9WZ8R(Wn5Rr zhc&dXvv_m%HrwwHo9l_))NgdVUff%d&@4^$Pc=MDZdZ^xHL$KX^ z7W1{3UJ%>9v$W{Y3>vBvflE-soDj8{`>#F|8Z$EF%lN$NylORTn5JsI4mTMHWd*%- z2sD(RO(H-&i8&Ge)5i12slI5VekYCZ)s8rv&_)194;vKY2m8DIC2{4<&xTM3HHxwT zd(42n)gCJ$O4I|8sJq07#0U7Yk7PjPK&bMdy-5b)OdhSsBo^|IB_H43@&F@tpdJR0 z#~)=UJdP|=)O{0(rVZnjbTtwHV^}&kfLJQP@R6rda;K;O>9J9bnW$BgbzOZ8aO{D8 zPuJ%=Nqg~rdzk-IW0ZC5I%cc;ek5~=lDXl4?gMOQQ!KE5Aq$9qeGFM6jFP;Xy6)%N zjg{q(E6fnF02P3L*tutbHRR-gyYK3g^y9H?GMtIs;ojG zY~3*C>qD)(8jz}89w|xfb7L`^d>AG#%D-uq=qz}(o9kzzrx0LSBX90ykr*5oM+YmoTRWe+Cj6aq^xnWRymLmE>krCpoC9K%2LT0aK0Y< zt@kUUrrj1WL9rmBB8B;WXqg-BztOiUZX-!`*a&-75+!WZ!R0OPiZz?w`Of4q#+(;m z`${Ea6GnTCY3`V2R8w*}knf)*`RA@(8k{Lp4VP;<+ z9O_z0_{3=HcVi z5)&QGEB_&$)mu@)(Z8zuw#>Gc6C>^O-FUZEo;TO1@$>-xu%`v`tMS3V-8R1pb5w&zP%&rAP2*5h z$k{jqReFXCJhJ?-{x(2j5gH_zQ>;#Ec*@bUqF0u}XB09+U-K}+jQd>)k#AOkr6M8x zHyhrfJ`99@Vzr_B@*p@`DxeJ#`jimavZ9ZV%v{mO0!%9$TY(f%_}BU~3R%QxmSdD1 z2Bp45R0C=8qtx-~+oULrzCMHMof!&H<~~>BhOu9t%ti7ERzy&MfeFI`yIK^$C)AW3 zNQRoy0G}{Z0U#b~iYF^Jc^xOlG#4#C=;O>}m0(@{S^B2chkhuBA^ur)c`E;iGC9@z z7%fqif|WXh26-3;GTi8YpXUOSVWuR&C%jb}s5V4o;X~?V>XaR)8gBIQvmh3-xs)|E z8CExUnh>Ngjb^6YLgG<K?>j`V4Zp4G4%h8vUG^ouv)P!AnMkAWurg1zX2{E)hFp5ex ziBTDWLl+>ihx>1Um{+p<{v-zS?fx&Ioeu#9;aON_P4|J-J)gPF2-0?yt=+nHsn^1G z2bM#YbR1hHRbR9Or49U3T&x=1c0%dKX4HI!55MQv`3gt5ENVMAhhgEp@kG2k+qT|<5K~u`9G7x z?eB%b2B#mq)&K}m$lwDv|MU~=Y(D2jO{j*Box$GUn=$90z6O^7F?7pn=P;{r4C8qa zv1n*5N7uIvTn`8$>}(74>Oqk=E7){#pHUFd5XRJ5ObMhqODTa}=V0;+a(7JZR-4<3 zBTvsqRwLh?*ZF)JWsWOkEq7*XMQ!G3Rmkdh7ZbM#v1~?jt((e2y}u}Ky>1qa&Y7m@ zveIzH@?5Gexr79*?sbZGkVS;s1U<7D(%~7HjAmzj$aDYv_FGl5JX@LW8>w=HCDl6W z%?rsr0)bErYJ5G1v&zjr{8=lW)ZYcstgZAuL}!0~8HAcgOm@nJ9cvOOtL@)Fpl2Dr z8876Lt<|1eF88Jx#C*XyGI)C5z_o!Os!t=Xy0$Kj^4fG1pb@16%g z+<)zJ1n1QO78g#$3yHj+(Smv`HW5y_-PP{h2A1UXMG-c%hMvHLbF6t}G>KA)H# z`AWL~>8JUT(iq7;zJr!Aj)AS+n{mRbA3aM+Gj}b#PhHdTM_NkwQm330EC9waM$=slPfxR1vmr!vf~t_M?a%`@`&tdE}ipY-p#Q#zhLK zd9eFC;PjIEAKLkRkO94{rTuNFqKbNUGtaNZRRbax9;|%2WbnGu!44#64RriY5u0O} z05G^e&JB?Wb*8^g)aM`yt|}~QJkKCipFNeyex~P~SFPVEafD(73rncKmm)m~&`O*YUyY9z7tO%ec7z@wWcoOr-ebP z1k+|y?d{>1jLC=s4B2tEhiTtu->WVJno&%%6bG46KuU9D`GEN!C!9chM>zd=cl0+- z^k>4rpkq7_iWGHtBvy$Q`dja2;1ZdYmF6cANU6{v>l1=fSKRpsTRonp@alC%p{bhU z>g+(%-)&_nDQ~#bq5;xo^06RggA&uH4RMVb6wt;oQI+`m_zt>SiI5hXkfEnn6@ZNk zh9KUr1jtt6lBg$O#TAoTRvwUtWeMP3EjnGoRPQppiNF(sX%|Q4@kIjas|WZWXSENO zfF#2yOb;%XO*LeOoAwlf{u7_39$x(w3xT~)2BNJ2l5u4n3a0NkNLT4yT);7fA?1Vt zCz*`hbw-doYa09E!05zcfOT0EOORY``E@D z5{v%@F~&|UfNt@>vrj66W5f>jy+G_8&VB9D0*>N!7_Nr=-x6N?A)M8>1~q(X34sXp zpA%@w&c};L7u*G3;(Qe=LFL}NbTF$|aX#A%P(h`-N=ZRxCvlG$>Klv}jo0MS|UR8qKq-1FokBJmrbTJjQ!k#Is0tY+0c)m4Gp80YzYD zEGXd~ihaihk;?xUknXNH?rssjzaF+l6?HnDQjVP$i=q}{lp_WbOTKKg}HPKW)2sW`L#NvgmaY0^b2Ldk|t{P6{L{>ym;Xgao1PrudBgEMRFb^ zkPJ6v0h^tJ>K@;maHk_|6Z>yFzq@YvDOeO6Ob_?P4Ey>kHiJv`Wlh_MX4fBY36f%^ zV#2t;$Rg&}!Kwifm z;TVZXMxw3~$--{&A8-6vnUZ#s4`Z-zQ#+y7UI8#Hgsc|ompLUc zqlAG!Ti>t{JzYF^5pM925*PUWUvDuYDGKhC4FMx45c`L#V7%V+88@|khLj|V=J9Un zJEcP5qVCzR6p{FK!nIY~TXo)tJ!{>CG;~&u;EPlnNrwJ=5)ke@hJosN!siM$8b2mM zmc&weo-rY{n1+%c`c<{AT3i zjF{p253Ul-)s5A+!8Dp7?viXAdH1+qlY%mK5pp?{pS1t!3qmmDOq2TnoV`F3<>(XK z1=gfH39N_~8O+~({MZX~+QHyB>vtgwK0@uqGkX^eaf$UFHiO#>LB*7@=c0o6`0muj zmH00_F#p)s3E*$A-zP+p2bvXARTg3)Lxh`tf~9X>7!Z^kHV`uE%V9+BiBG=mxj*)M zr%3rn=)>GR`{#zmwD)$3ToLMx++uqsCx(+50Uk*5QJp2c6msxLD&P-y{c|XK6zZl3 z_Fgu8kp|gKVWv`GS!c56FWPO)ZrCCtYh#*yp-ssus)ot>_~UB zyGfjTjz#fXod{^KEQK1~@jN|;SZw5OgH#0wK78Oe4#vV3*|&XPQU z$r~5u8ziT0<#ICrX^<1){mvtaqT9OqlW?wiSu4X#rOC(0uL{Ownb%i1F_G&d>=l51 zx!FEO4_LK+)W^N6UF+fAccyyp{t)TE`;vF@1irbNjcXF8b?yFh zl5UEB>@;wO`~gMF!QB;h<``+f(lxAb_8B$;&vT7)(bXG(7x_5f%AZ5;h#3WjHisX{ zLTSguapAADXMwWZ&jsD0+K!+8#*6z7-(T+QUk>(~!Q|0&!d)PgEw8F6RK;LkB;!HXg79$+l*KU&-fRF|$o+kR4mJ36k9p&>*uS~RhCV+*Y$3U-k%~M)jxCFW zl9;bQ-fx4HPy)*(bhrKL!81M6*@6p5W?z*W`jb;@JKMFwmic{gQPv*) z?I{Fh)y)}(-6uh^I52xKo!LRZV0c*1X)Z(g+GVFN{2n%vD*@&IkVI{R_0;M28M z8vu?M+xVF-&<{l@1g{PA#hnyAq(gudz4WKSFL5YOr3q!|qrxa7z~F~rEJ29VQKgNe z1*L^m9&acg2p7&`u&V%oY|AKF(Xpv=)wf&j#n|;2UYEaUIHLJuTQw$SbrNn+)38PlfV^0<6s>)|hT#IAAS*T)_^_q@I} z0S%tV-HrXOjzkvW!YSbDjdH=g;=4A@whsDB zI8^aX6n=|ab(?!Ay!)CxH(wC(iX~Q@%FEx>C{Hmp98f2ku$Bsw%lk6v50(U@; zu68Z9U&za}O#-Mv^+!V=eyj6S)5oS{My`1MVs)nlnYl_$xU^QId1_jMf7&K8ij)jQ zJ|+~@l)xpV%~Y{P()$`+nBihkjE|3t3t8PoKU3wZ_Eg%0P<>%(A@oW#*8i$X!nfG& z;&&2ZIKlD~*Gff+p3A7QB!}Ei>RGhUUz^UoEpeJ{`2ov>wH!O@1$VW>A#D#{i2z9l z{d)FK9OYxRY#(6NUMO=q^5Ve7R|72%f}ZDlsm0BN&LzyaSHurXV4p5HGf7|Z)}8)g z5J#S6h{-+_U0m$k#+|N{6_8MYactWzWb+1~ea8wX3zX<@O0>pU*q($J{=R&7)P&jg z6Kb)o=HAnC_MP;cIeBq}{gG^0CZzOUJZ|7C-VjE}!?*UtKTcwwF33v^BYC&}Rq)C* zpAJ07-!{`flYX1@n;ZK-=x4)!o(%(1UqulVmes(D z^`_HNfM#umEYy~=zh$9&+?8$4!l(4rr?d#8hS4iks@9w%E4l`BKmhUtvsm1X-mKC3 z>4(u4yS45OgZIOQ;EQ6s`sjNelo!~mLe7gS69TW2WnFwEKcAwioq2mLXV<9CIa#(0`sQpl>vwW`A$D?!2%nt*HEb;Ga=o?92 zHAOICmXHEQ%Cc{m2>dLjPU1J}^w7zilFIxy9nG(OZbYPtW?3KJyv@A7|1A*NiD_v! zTLC}%E4kI*d?$lQBRL==MPsD#FyN0ZSr`;aeQ4C6a2INH9klU~_gCH;G2%8R4EuHb z44Ej^6301>?c06FP3X~xyP{77p`-3td;HKAGf4mZw1qRd6Z^^L#?qaiAKv~px)*jAV^re~beps9m{kJzb6n(oS8uCt#Lnjofg;Rl z=apY)JsV;^dVkzCW)jDrii_WTT`3iKri(xmCC1^AO}Vqt-1B*wwIlBAmE1AmdRtMc zD!fB@mtwHPHyV-^VIVU??*~*{olz-Ub)NCX941BDj_CKZ+QYQ?+``tyhy_7WFXF}_ z?~CVO#LsDYD!&}cph22{PZ*TK?$K^u`E7%{^na89Rm%!jSZs7vI-D zL1POD!1cu56G)*p1gui3-i^JZPX3tI*_Fq&JRwbz*#8LUSiMRWjuu`zD|uk;+X&d@ zuxF5C2{Zp#O?GtOB+R2~tF>MDI(}%p-W=M>1tEY}8E=b_l*WbOO zY9tCPgL3vMEqz)_eWeqmN{qobq_4)XdXJSe6Hj;Eie0??2ZZ?p;*_K8@(&v~1evu- zxQCA2YYvv@qhzamqdi`?{Z{c*7$arCdz4-4G(`O5It%y&8>d{#Y9Vax^FZ99ZK zUdIPpkNhp8uP3T+W4lhvUIYaoY##y6KtxBFoj3&5^@Q(^{677%C#3YJh$p-Ee2M6F ztJAoQv1N0L!|N8XBD(eAYcB#gRaIX7T8U5xXbx~cJSon~YnC zaJYE%zOj9y?E==_B$*9NiAm{~)2Z}t1$$l?qOYct5Ep5HvqFKvuSE7A5YF$K@2>UE zbQOdTNzjD#zS(L>wa2$K-WK!Pc%pY^8To58;^JaXZ}F30wuYl;WWs~rCoo&vrEtUh zTBLMU??yx1#;-weCPZyOJ%Yeb?14z+OXW0L_E+<)(q=;xz74U-Q~R~n*oC;MxyrJo(74r$y2t;x`D~{nhUw`N{Bbc zo`l5kb`Yy;L=&@MTQ~Ml_%V%){mCIj4WC}5q=A_ACx2^by!4w1rVX6H0ifayJsw;; z=+}5kjC?RG*q)^FA;udd?fK$7vU1x>y0w;A-)YbE%l$J%nRRjAIlrItFPgQvJ7Ytb z%HSFnjF2||X&L_g-Q>1{(mholW_-EJmSzsO%*VVVB4)#OAv<(kOIx2H!f)I9#e_Nyjdb$&*1KN^gM}yFIhi%%BWB}7Ke0M{0WY>CxJQUuL<9GW$I>S z8~;QmE{^wS?I`=DyV^l+MozMPWLoFz=uSLu99tiVHdCN>7jRs~vd13`&Gey!!7_+< z6o@25%!eN~+Eki#7iq@#{Hxl7pF0^`N;~p~#tc6HXJP0g5xvK|AuLSwNHVI2_Y-!& z4hemc%vOM5!ySDypyEGe=lAeFbIp`w8FIUcTqUwens>sTIV-jDhrcKGX7XHFXyazb z^DO8=ZgefY6R6&+)c1_i*WoenjtR5@_JU#Ph;4M8fpmznxE9R`=r@-#_y zkD?Muq|*gg7f*BQeI|Np#}Q|NXLJHM6GE{;SJn8ce`V1Gehym~{8c+M<2~=HcCRuk z-v&$8dc8YG+tK}NYVhwdm1iZ&A#r+T<>Ez88)Eq9j+G5h5D(_u{WQdUTOs+QbA(=? z{F6n6UV8D2*lvb)0vDrca$729KG$xO2aH$jWoWl0drlmefYsTswh)`GjMtmR=vEkJ zN$aTp_@@KL%KQ-VDB2ppbZK@X`6cJA5n`g>sbCTvU_xdid!{9gWA|>Mfs6rtHx6s` z_wMt*FgUTBZ@I2C62&zbs?pPvK9TpatkXzqDqe4YTr^nnQg8gWxjKt*s&eOMEp!Qc zG~PT`>xg76Xqh^dKI-Eu#K*VnvEf9qT{L0yNpVj)eVD#kQzGgVRbTB!5nWY=?t!cggiEGBAcWM2xNtW&9 zZB_6RZ}|a87CuEYRYCRJ`Sg+_gBK$_J@*zoWcJJw>eBw?G9WY(Jw~qN|A3MBR^~jm?>k5oGv7z+0jWOox(co@%nya|* zE-2peyX)#@svgwwDMPJ89dT=iO>}@wtNR@NUQ|cJZ};sX(w2uWP4AE5)@A ziJgy_TIZ+T&vG&xPh@Jmt!OJ|zA6C0ZxfF2 z7>aIZqecbmM$lyvDMwg2?Ipo9b)-WL6K_7(X_rmJgdd$-Qc^ywEw4SThChz6*_yu= z{v~a4V|RJtH-GThc2C0Z|JHPl{II-!?B~7cWnRz&dgP*UqoY!iCo&i-xeM}kl?ID* zKTX`w+;z0+MCdGcl{N?xb|tYb%Id=k++k_@(V%bTS&n09`0{S0)|>IH_F;V@_zrxS-dKDDc7+i`nHN8J z;38w69lzAS*WWa+dnVvk(0-KD3%*)TerLH zSCc}Tjc-mR5|1HAL$C1}oue|Qp&M!hmyDUcg)Cz>GXPEyeYf}+s48kIl*pL{{treP BIP(Ai literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0a6f07e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + ParseApplication + ETyShFeRiPBRMNHWMPafdPxrgybq2Tu8Chk0xlC6 + https://parseapi.back4app.com/ + wfLFnzULRBHwNiSyC85tHKYZ0uTxpMWOnxemmv9Z + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/net/adphi/apps/parseapplication/ExampleUnitTest.kt b/app/src/test/java/net/adphi/apps/parseapplication/ExampleUnitTest.kt new file mode 100644 index 0000000..5d68859 --- /dev/null +++ b/app/src/test/java/net/adphi/apps/parseapplication/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package net.adphi.apps.parseapplication + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5be5108 --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.2.20' + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.0.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +ext { + compileSdkVersion = 26 + minSdkVersion = 16 + targetSdkVersion = 26 + + supportLibVersion = '26.1.0' + + okhttpVersion = '3.9.1' +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa1b867 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Mar 25 12:32:42 CEST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3a7bf66 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include ':app', ':Parse', ':ParseLiveQuery' +project(':Parse').projectDir = new File('ExternalLibs/Parse-SDK-Android/Parse') +project(':ParseLiveQuery').projectDir = new File('ExternalLibs/ParseLiveQuery-Android/ParseLiveQuery') \ No newline at end of file