From 95c2056e1cac02921bc7ce8c84860baa9aee3da2 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 11:21:58 -0400 Subject: [PATCH 01/10] clone database testapp --- functions/testapp/AndroidManifest.xml | 22 + functions/testapp/CMakeLists.txt | 106 ++ functions/testapp/LICENSE | 202 ++++ functions/testapp/LaunchScreen.storyboard | 7 + functions/testapp/Podfile | 8 + functions/testapp/build.gradle | 122 ++ .../testapp/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + functions/testapp/gradlew | 164 +++ functions/testapp/gradlew.bat | 90 ++ functions/testapp/jni/Android.mk | 64 + functions/testapp/jni/Application.mk | 20 + functions/testapp/proguard.pro | 2 + functions/testapp/readme.md | 226 ++++ functions/testapp/res/layout/main.xml | 12 + functions/testapp/res/values/strings.xml | 4 + functions/testapp/src/android/android_main.cc | 255 ++++ .../google/firebase/example/LoggingUtils.java | 55 + functions/testapp/src/common_main.cc | 1054 +++++++++++++++++ functions/testapp/src/desktop/desktop_main.cc | 125 ++ functions/testapp/src/ios/ios_main.mm | 120 ++ functions/testapp/src/main.h | 63 + .../testapp/testapp.xcodeproj/project.pbxproj | 312 +++++ .../AppIcon.appiconset/Contents.json | 58 + .../LaunchImage.launchimage/Contents.json | 51 + functions/testapp/testapp/Info.plist | 39 + 26 files changed, 3187 insertions(+) create mode 100644 functions/testapp/AndroidManifest.xml create mode 100644 functions/testapp/CMakeLists.txt create mode 100644 functions/testapp/LICENSE create mode 100644 functions/testapp/LaunchScreen.storyboard create mode 100644 functions/testapp/Podfile create mode 100644 functions/testapp/build.gradle create mode 100644 functions/testapp/gradle/wrapper/gradle-wrapper.jar create mode 100644 functions/testapp/gradle/wrapper/gradle-wrapper.properties create mode 100644 functions/testapp/gradlew create mode 100644 functions/testapp/gradlew.bat create mode 100644 functions/testapp/jni/Android.mk create mode 100644 functions/testapp/jni/Application.mk create mode 100644 functions/testapp/proguard.pro create mode 100644 functions/testapp/readme.md create mode 100644 functions/testapp/res/layout/main.xml create mode 100644 functions/testapp/res/values/strings.xml create mode 100644 functions/testapp/src/android/android_main.cc create mode 100644 functions/testapp/src/android/java/com/google/firebase/example/LoggingUtils.java create mode 100644 functions/testapp/src/common_main.cc create mode 100644 functions/testapp/src/desktop/desktop_main.cc create mode 100644 functions/testapp/src/ios/ios_main.mm create mode 100644 functions/testapp/src/main.h create mode 100644 functions/testapp/testapp.xcodeproj/project.pbxproj create mode 100644 functions/testapp/testapp/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 functions/testapp/testapp/Images.xcassets/LaunchImage.launchimage/Contents.json create mode 100644 functions/testapp/testapp/Info.plist diff --git a/functions/testapp/AndroidManifest.xml b/functions/testapp/AndroidManifest.xml new file mode 100644 index 00000000..db49c150 --- /dev/null +++ b/functions/testapp/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/functions/testapp/CMakeLists.txt b/functions/testapp/CMakeLists.txt new file mode 100644 index 00000000..3a40cb84 --- /dev/null +++ b/functions/testapp/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 2.8) + +# User settings for Firebase samples. +# Path to Firebase SDK. +# Try to read the path to the Firebase C++ SDK from an environment variable. +if (NOT "$ENV{FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(DEFAULT_FIREBASE_CPP_SDK_DIR "$ENV{FIREBASE_CPP_SDK_DIR}") +else() + set(DEFAULT_FIREBASE_CPP_SDK_DIR "firebase_cpp_sdk") +endif() +if ("${FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(FIREBASE_CPP_SDK_DIR ${DEFAULT_FIREBASE_CPP_SDK_DIR}) +endif() +if(NOT EXISTS ${FIREBASE_CPP_SDK_DIR}) + message(FATAL_ERROR "The Firebase C++ SDK directory does not exist: ${FIREBASE_CPP_SDK_DIR}. See the readme.md for more information") +endif() + +# Windows runtime mode, either MD or MT depending on whether you are using +# /MD or /MT. For more information see: +# https://msdn.microsoft.com/en-us/library/2kzt1wy3.aspx +set(MSVC_RUNTIME_MODE MD) + +project(firebase_testapp) + +# Sample source files. +set(FIREBASE_SAMPLE_COMMON_SRCS + src/main.h + src/common_main.cc +) + +# Platform abstraction layer for the sample. +set(FIREBASE_SAMPLE_DESKTOP_SRCS + src/desktop/desktop_main.cc +) + +# Sample uses some features that require C++ 11, such as lambdas. +set (CMAKE_CXX_STANDARD 11) + +# Determine the path to the library based on the platform and configuration. +if(APPLE) + set(FIREBASE_SDK_LIBDIR ${FIREBASE_CPP_SDK_DIR}/libs/darwin/universal) + set(ADDITIONAL_LIBS + pthread + "-framework CoreFoundation" + "-framework Security" + ) +elseif(MSVC) + if(${CMAKE_CL_64}) + set(MSVC_CPU x64) + else() + set(MSVC_CPU x86) + endif() + if(CMAKE_BUILD_TYPE EQUAL Release) + set(MSVC_CONFIG Release) + else() + set(MSVC_CONFIG Debug) + endif() + set(MSVC_VS_VERSION VS2015) + set(FIREBASE_SDK_LIBDIR + ${FIREBASE_CPP_SDK_DIR}/libs/windows/${MSVC_VS_VERSION}/${MSVC_RUNTIME_MODE}/${MSVC_CPU}/${MSVC_CONFIG}) + set(ADDITIONAL_LIBS advapi32 ws2_32 crypt32 iphlpapi psapi userenv) +else() + # The Firebase libraries are not built with glibcxx11, so disable the ABI. + add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0) + set(LINUX_CPU x86_64) + set(FIREBASE_SDK_LIBDIR ${FIREBASE_CPP_SDK_DIR}/libs/linux/${LINUX_CPU}) + set(ADDITIONAL_LIBS pthread) +endif() + +# Link Firebase libraries. +# NOTE: firebase_app needs to be after all other Firebase libraries. +link_directories(${FIREBASE_SDK_LIBDIR}) +set(FIREBASE_LIBS firebase_database firebase_auth firebase_app) + +# Add the Firebase include directory. +set(FIREBASE_SDK_INCLUDEDIR ${FIREBASE_CPP_SDK_DIR}/include) +include_directories(${FIREBASE_SDK_INCLUDEDIR}) + +# The include directory for the testapp. +include_directories(src) + +add_executable(desktop_testapp + ${FIREBASE_SAMPLE_DESKTOP_SRCS} + ${FIREBASE_SAMPLE_COMMON_SRCS} +) +target_link_libraries(desktop_testapp + ${FIREBASE_LIBS} + ${ADDITIONAL_LIBS} +) + +# If a config file is present, copy it into the binary location so that it's +# possible to create the default Firebase app. +set(FOUND_JSON_FILE FALSE) +foreach(config "google-services-desktop.json" "google-services.json") + if (EXISTS ${config}) + add_custom_command( + TARGET desktop_testapp POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${config} $) + set(FOUND_JSON_FILE TRUE) + break() + endif() +endforeach() +if(NOT FOUND_JSON_FILE) + message(WARNING "Failed to find either google-services-desktop.json or google-services.json. See the readme.md for more information.") +endif() diff --git a/functions/testapp/LICENSE b/functions/testapp/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/functions/testapp/LICENSE @@ -0,0 +1,202 @@ + + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/functions/testapp/LaunchScreen.storyboard b/functions/testapp/LaunchScreen.storyboard new file mode 100644 index 00000000..673e0f7e --- /dev/null +++ b/functions/testapp/LaunchScreen.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/functions/testapp/Podfile b/functions/testapp/Podfile new file mode 100644 index 00000000..c3c902b9 --- /dev/null +++ b/functions/testapp/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +# Firebase Realtime Database test application. +target 'testapp' do + pod 'Firebase/Core' + pod 'Firebase/Database' + pod 'Firebase/Auth' +end diff --git a/functions/testapp/build.gradle b/functions/testapp/build.gradle new file mode 100644 index 00000000..fcb9a0aa --- /dev/null +++ b/functions/testapp/build.gradle @@ -0,0 +1,122 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + mavenLocal() + maven { url 'https://maven.google.com' } + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.google.gms:google-services:4.0.1' + } +} + +allprojects { + repositories { + mavenLocal() + maven { url 'https://maven.google.com' } + jcenter() + } +} + +apply plugin: 'com.android.application' + +// Pre-experimental Gradle plug-in NDK boilerplate below. +// Right now the Firebase plug-in does not work with the experimental +// Gradle plug-in so we're using ndk-build for the moment. +project.ext { + // Configure the Firebase C++ SDK location. + firebase_cpp_sdk_dir = System.getProperty('firebase_cpp_sdk.dir') + if (firebase_cpp_sdk_dir == null || firebase_cpp_sdk_dir.isEmpty()) { + firebase_cpp_sdk_dir = System.getenv('FIREBASE_CPP_SDK_DIR') + if (firebase_cpp_sdk_dir == null || firebase_cpp_sdk_dir.isEmpty()) { + if ((new File('firebase_cpp_sdk')).exists()) { + firebase_cpp_sdk_dir = 'firebase_cpp_sdk' + } else { + throw new StopActionException( + 'firebase_cpp_sdk.dir property or the FIREBASE_CPP_SDK_DIR ' + + 'environment variable must be set to reference the Firebase C++ ' + + 'SDK install directory. This is used to configure static library ' + + 'and C/C++ include paths for the SDK.') + } + } + } + if (!(new File(firebase_cpp_sdk_dir)).exists()) { + throw new StopActionException( + sprintf('Firebase C++ SDK directory %s does not exist', + firebase_cpp_sdk_dir)) + } + // Check the NDK location using the same configuration options as the + // experimental Gradle plug-in. + ndk_dir = project.android.ndkDirectory + if (ndk_dir == null || !ndk_dir.exists()) { + ndk_dir = System.getenv('ANDROID_NDK_HOME') + if (ndk_dir == null || ndk_dir.isEmpty()) { + throw new StopActionException( + 'Android NDK directory should be specified using the ndk.dir ' + + 'property or ANDROID_NDK_HOME environment variable.') + } + } +} + +android { + compileSdkVersion 26 + buildToolsVersion '26.0.3' + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/android/java'] + res.srcDirs = ['res'] + } + } + + defaultConfig { + applicationId 'com.google.firebase.cpp.database.testapp' + minSdkVersion 14 + targetSdkVersion 26 + versionCode 1 + versionName '1.0' + } + buildTypes { + release { + minifyEnabled true + proguardFile getDefaultProguardFile('proguard-android.txt') + proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/app.pro") + proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/auth.pro") + proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/database.pro") + proguardFile file('proguard.pro') + } + } +} + +dependencies { + compile 'com.google.firebase:firebase-core:16.0.0' + compile 'com.google.firebase:firebase-auth:16.0.1' + compile 'com.google.firebase:firebase-database:16.0.1' +} + +apply plugin: 'com.google.gms.google-services' + +task ndkBuildCompile(type:Exec) { + description 'Use ndk-build to compile the C++ application.' + commandLine("${project.ext.ndk_dir}${File.separator}ndk-build", + "FIREBASE_CPP_SDK_DIR=${project.ext.firebase_cpp_sdk_dir}", + sprintf("APP_PLATFORM=android-%d", + android.defaultConfig.minSdkVersion.mApiLevel)) +} + +task ndkBuildClean(type:Exec) { + description 'Use ndk-build to clean the C++ application.' + commandLine("${project.ext.ndk_dir}${File.separator}ndk-build", + "FIREBASE_CPP_SDK_DIR=${project.ext.firebase_cpp_sdk_dir}", + "clean") +} + +// Once the Android Gradle plug-in has generated tasks, add dependencies for +// the ndk-build targets. +project.afterEvaluate { + preBuild.dependsOn(ndkBuildCompile) + clean.dependsOn(ndkBuildClean) +} diff --git a/functions/testapp/gradle/wrapper/gradle-wrapper.jar b/functions/testapp/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c0fb64a8698b08ecc4158d828ca593c4928e9dd GIT binary patch literal 49896 zcmagFb986H(k`5d^NVfUwr$(C?M#x1ZQHiZiEVpg+jrjgoQrerx!>1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z 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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/functions/testapp/gradlew.bat b/functions/testapp/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/functions/testapp/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/functions/testapp/jni/Android.mk b/functions/testapp/jni/Android.mk new file mode 100644 index 00000000..cb38dc98 --- /dev/null +++ b/functions/testapp/jni/Android.mk @@ -0,0 +1,64 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# 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. + +LOCAL_PATH:=$(call my-dir)/.. + +ifeq ($(FIREBASE_CPP_SDK_DIR),) +$(error FIREBASE_CPP_SDK_DIR must specify the Firebase package location.) +endif + +# With Firebase libraries for the selected build configuration (ABI + STL) +STL:=$(firstword $(subst _, ,$(APP_STL))) +FIREBASE_LIBRARY_PATH:=\ +$(FIREBASE_CPP_SDK_DIR)/libs/android/$(TARGET_ARCH_ABI)/$(STL) + +include $(CLEAR_VARS) +LOCAL_MODULE:=firebase_app +LOCAL_SRC_FILES:=$(FIREBASE_LIBRARY_PATH)/libfirebase_app.a +LOCAL_EXPORT_C_INCLUDES:=$(FIREBASE_CPP_SDK_DIR)/include +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE:=firebase_auth +LOCAL_SRC_FILES:=$(FIREBASE_LIBRARY_PATH)/libfirebase_auth.a +LOCAL_EXPORT_C_INCLUDES:=$(FIREBASE_CPP_SDK_DIR)/include +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE:=firebase_database +LOCAL_SRC_FILES:=$(FIREBASE_LIBRARY_PATH)/libfirebase_database.a +LOCAL_EXPORT_C_INCLUDES:=$(FIREBASE_CPP_SDK_DIR)/include +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE:=android_main +LOCAL_SRC_FILES:=\ + $(LOCAL_PATH)/src/common_main.cc \ + $(LOCAL_PATH)/src/android/android_main.cc +LOCAL_STATIC_LIBRARIES:=\ + firebase_database \ + firebase_auth \ + firebase_app +LOCAL_WHOLE_STATIC_LIBRARIES:=\ + android_native_app_glue +LOCAL_C_INCLUDES:=\ + $(NDK_ROOT)/sources/android/native_app_glue \ + $(LOCAL_PATH)/src +LOCAL_LDLIBS:=-llog -landroid -latomic +LOCAL_ARM_MODE:=arm +LOCAL_LDFLAGS:=-Wl,-z,defs -Wl,--no-undefined +include $(BUILD_SHARED_LIBRARY) + +$(call import-add-path,$(NDK_ROOT)/sources/android) +$(call import-module,android/native_app_glue) diff --git a/functions/testapp/jni/Application.mk b/functions/testapp/jni/Application.mk new file mode 100644 index 00000000..4904a6c8 --- /dev/null +++ b/functions/testapp/jni/Application.mk @@ -0,0 +1,20 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# 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. + +APP_PLATFORM:=android-14 +NDK_TOOLCHAIN_VERSION=clang +APP_ABI:=armeabi-v7a arm64-v8a x86 x86_64 +APP_STL:=c++_static +APP_MODULES:=android_main +APP_CPPFLAGS+=-std=c++11 diff --git a/functions/testapp/proguard.pro b/functions/testapp/proguard.pro new file mode 100644 index 00000000..54cd248b --- /dev/null +++ b/functions/testapp/proguard.pro @@ -0,0 +1,2 @@ +-ignorewarnings +-keep,includedescriptorclasses public class com.google.firebase.example.LoggingUtils { *; } diff --git a/functions/testapp/readme.md b/functions/testapp/readme.md new file mode 100644 index 00000000..27100e87 --- /dev/null +++ b/functions/testapp/readme.md @@ -0,0 +1,226 @@ +Firebase Realtime Database Quickstart +======================== + +The Firebase Realtime Database Test Application (testapp) demonstrates Firebase +Realtime Database operations with the Firebase Realtime Database C++ SDK. The +application has no user interface and simply logs actions it's performing to the +console. + +The testapp performs the following: + - Creates a firebase::App in a platform-specific way. The App holds + platform-specific context that's used by other Firebase APIs, and is a + central point for communication between the Firebase Realtime Database C++ + and Firebase Auth C++ libraries. + - Gets a pointer to firebase::Auth, and signs in anonymously. This allows the + testapp to access a Firebase Database instance with authentication rules + enabled, which is the default setting in Firebase Console. + - Gets a DatabaseReference to the root node's "test_app_data" child, uses + DatabaseReference::PushChild() to create a child with a unique key + underneath it to work in, and gets a reference to that child, which the + testapp will use for the remainder of its actions. + - Sets some simple values (numbers, bools, strings, timestamp) and reads them + back to ensure the database can be read from and written to. + - Runs a transaction, using DatabaseReference::RunTransaction(), and validates + that its results were applied properly. + - Runs DatabaseReference::UpdateChildren to update multiple children at once. + - Uses Query to narrow down the view from a DatabaseReference. + - Sets up a ValueListener to watch for data value changes at a given database + location. + - Sets up a ChildListener to watch for changes in the list of children at + a database location. + - Sets up OnDisconnect actions to make changes to the database on disconnect, + then disconnects from the database to confirm the actions are performed. + - Shuts down the Firebase Database, Firebase Auth, and Firebase App systems. + +Introduction +------------ + +- [Read more about Firebase Realtime Database](https://firebase.google.com/docs/database/) + +Building and Running the testapp +-------------------------------- + +### iOS + - Link your iOS app to the Firebase libraries. + - Get CocoaPods version 1 or later by running, + ``` + sudo gem install cocoapods --pre + ``` + - From the testapp directory, install the CocoaPods listed in the Podfile + by running, + ``` + pod install + ``` + - Open the generated Xcode workspace (which now has the CocoaPods), + ``` + open testapp.xcworkspace + ``` + - For further details please refer to the + [general instructions for setting up an iOS app with Firebase](https://firebase.google.com/docs/ios/setup). + - Register your iOS app with Firebase. + - Create a new app on the [Firebase console](https://firebase.google.com/console/), and attach + your iOS app to it. + - You can use "com.google.firebase.cpp.database.testapp" as the iOS Bundle + ID while you're testing. You can omit App Store ID while testing. + - Add the GoogleService-Info.plist that you downloaded from Firebase + console to the testapp root directory. This file identifies your iOS app + to the Firebase backend. + - In the Firebase console for your app, select "Auth", then enable + "Anonymous". This will allow the testapp to use anonymous sign-in to + authenticate with Firebase Database, which requires a signed-in user by + default (an anonymous user will suffice). + - Download the Firebase C++ SDK linked from + [https://firebase.google.com/docs/cpp/setup]() and unzip it to a + directory of your choice. + - Add the following frameworks from the Firebase C++ SDK to the project: + - frameworks/ios/universal/firebase.framework + - frameworks/ios/universal/firebase_auth.framework + - frameworks/ios/universal/firebase_database.framework + - You will need to either, + 1. Check "Copy items if needed" when adding the frameworks, or + 2. Add the framework path in "Framework Search Paths" + - e.g. If you downloaded the Firebase C++ SDK to + `/Users/me/firebase_cpp_sdk`, + then you would add the path + `/Users/me/firebase_cpp_sdk/frameworks/ios/universal`. + - To add the path, in XCode, select your project in the project + navigator, then select your target in the main window. + Select the "Build Settings" tab, and click "All" to see all + the build settings. Scroll down to "Search Paths", and add + your path to "Framework Search Paths". + - In XCode, build & run the sample on an iOS device or simulator. + - The testapp has no interative interface. The output of the app can be viewed + via the console or on the device's display. In Xcode, select + "View --> Debug Area --> Activate Console" from the menu to view the console. + +### Android + - Register your Android app with Firebase. + - Create a new app on + the [Firebase console](https://firebase.google.com/console/), and attach + your Android app to it. + - You can use "com.google.firebase.cpp.database.testapp" as the Package + Name while you're testing. + - To + [generate a SHA1](https://developers.google.com/android/guides/client-auth) + run this command on Mac and Linux, + ``` + keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore + ``` + or this command on Windows, + ``` + keytool -exportcert -list -v -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore + ``` + - If keytool reports that you do not have a debug.keystore, you can + [create one with](http://developer.android.com/tools/publishing/app-signing.html#signing-manually), + ``` + keytool -genkey -v -keystore ~/.android/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US" + ``` + - Add the `google-services.json` file that you downloaded from Firebase + console to the root directory of testapp. This file identifies your + Android app to the Firebase backend. + - In the Firebase console for your app, select "Auth", then enable + "Anonymous". This will allow the testapp to use anonymous sign-in to + authenticate with Firebase Database, which requires a signed-in user by + default (an anonymous user will suffice). + - For further details please refer to the + [general instructions for setting up an Android app with Firebase](https://firebase.google.com/docs/android/setup). + - Download the Firebase C++ SDK linked from + [https://firebase.google.com/docs/cpp/setup]() and unzip it to a + directory of your choice. + - Configure the location of the Firebase C++ SDK by setting the + firebase\_cpp\_sdk.dir Gradle property to the SDK install directory. + For example, in the project directory: + ``` + echo "systemProp.firebase\_cpp\_sdk.dir=/User/$USER/firebase\_cpp\_sdk" >> gradle.properties + ``` + - Ensure the Android SDK and NDK locations are set in Android Studio. + - From the Android Studio launch menu, go to `File/Project Structure...` or + `Configure/Project Defaults/Project Structure...` + (Shortcut: Control + Alt + Shift + S on windows, Command + ";" on a mac) + and download the SDK and NDK if the locations are not yet set. + - Open *build.gradle* in Android Studio. + - From the Android Studio launch menu, "Open an existing Android Studio + project", and select `build.gradle`. + - Install the SDK Platforms that Android Studio reports missing. + - Build the testapp and run it on an Android device or emulator. + - The testapp has no interactive interface. The output of the app can be + viewed on the device's display, or in the logcat output of Android studio or + by running "adb logcat *:W android_main firebase" from the command line. + +### Desktop + - Register your app with Firebase. + - Create a new app on the [Firebase console](https://firebase.google.com/console/), + following the above instructions for Android or iOS. + - If you have an Android project, add the `google-services.json` file that + you downloaded from the Firebase console to the root directory of the + testapp. + - If you have an iOS project, and don't wish to use an Android project, + you can use the Python script `generate_xml_from_google_services_json.py --plist`, + located in the Firebase C++ SDK, to convert your `GoogleService-Info.plist` + file into a `google-services-desktop.json` file, which can then be + placed in the root directory of the testapp. + - Download the Firebase C++ SDK linked from + [https://firebase.google.com/docs/cpp/setup]() and unzip it to a + directory of your choice. + - Configure the testapp with the location of the Firebase C++ SDK. + This can be done a couple different ways (in highest to lowest priority): + - When invoking cmake, pass in the location with + -DFIREBASE_CPP_SDK_DIR=/path/to/firebase_cpp_sdk. + - Set an environment variable for FIREBASE_CPP_SDK_DIR to the path to use. + - Edit the CMakeLists.txt file, changing the FIREBASE_CPP_SDK_DIR path + to the appropriate location. + - From the testapp directory, generate the build files by running, + ``` + cmake . + ``` + If you want to use XCode, you can use -G"Xcode" to generate the project. + Similarly, to use Visual Studio, -G"Visual Studio 15 2017". For more + information, see + [CMake generators](https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html). + - Build the testapp, by either opening the generated project file based on + the platform, or running, + ``` + cmake --build . + ``` + - Execute the testapp by running, + ``` + ./desktop_testapp + ``` + Note that the executable might be under another directory, such as Debug. + - The testapp has no user interface, but the output can be viewed via the + console. + +Known issues +------------ + + - Due to the way Firebase Realtime Database interacts with JSON, it is + possible that if you set a location to an integral value using a + firebase::Variant of type Int64, you may get back a firebase::Variant of + type Double when later retrieving the data from the database. Be sure to + check the types of Variants or use methods such as Variant::AsInt64() to + coerce their types before accessing the data within them. + +Support +------- + +[https://firebase.google.com/support/]() + +License +------- + +Copyright 2016 Google, Inc. + +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. diff --git a/functions/testapp/res/layout/main.xml b/functions/testapp/res/layout/main.xml new file mode 100644 index 00000000..d3ffb630 --- /dev/null +++ b/functions/testapp/res/layout/main.xml @@ -0,0 +1,12 @@ + + + + diff --git a/functions/testapp/res/values/strings.xml b/functions/testapp/res/values/strings.xml new file mode 100644 index 00000000..eef9e385 --- /dev/null +++ b/functions/testapp/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Firebase Database Test + diff --git a/functions/testapp/src/android/android_main.cc b/functions/testapp/src/android/android_main.cc new file mode 100644 index 00000000..73cb30e7 --- /dev/null +++ b/functions/testapp/src/android/android_main.cc @@ -0,0 +1,255 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// 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. + +#include +#include + +#include +#include +#include +#include + +#include "main.h" // NOLINT + +// This implementation is derived from http://github.com/google/fplutil + +extern "C" int common_main(int argc, const char* argv[]); + +static struct android_app* g_app_state = nullptr; +static bool g_destroy_requested = false; +static bool g_started = false; +static bool g_restarted = false; +static pthread_mutex_t g_started_mutex; + +// Handle state changes from via native app glue. +static void OnAppCmd(struct android_app* app, int32_t cmd) { + g_destroy_requested |= cmd == APP_CMD_DESTROY; +} + +// Process events pending on the main thread. +// Returns true when the app receives an event requesting exit. +bool ProcessEvents(int msec) { + struct android_poll_source* source = nullptr; + int events; + int looperId = ALooper_pollAll(msec, nullptr, &events, + reinterpret_cast(&source)); + if (looperId >= 0 && source) { + source->process(g_app_state, source); + } + return g_destroy_requested | g_restarted; +} + +// Get the activity. +jobject GetActivity() { return g_app_state->activity->clazz; } + +// Get the window context. For Android, it's a jobject pointing to the Activity. +jobject GetWindowContext() { return g_app_state->activity->clazz; } + +// Find a class, attempting to load the class if it's not found. +jclass FindClass(JNIEnv* env, jobject activity_object, const char* class_name) { + jclass class_object = env->FindClass(class_name); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + // If the class isn't found it's possible NativeActivity is being used by + // the application which means the class path is set to only load system + // classes. The following falls back to loading the class using the + // Activity before retrieving a reference to it. + jclass activity_class = env->FindClass("android/app/Activity"); + jmethodID activity_get_class_loader = env->GetMethodID( + activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); + + jobject class_loader_object = + env->CallObjectMethod(activity_object, activity_get_class_loader); + + jclass class_loader_class = env->FindClass("java/lang/ClassLoader"); + jmethodID class_loader_load_class = + env->GetMethodID(class_loader_class, "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + jstring class_name_object = env->NewStringUTF(class_name); + + class_object = static_cast(env->CallObjectMethod( + class_loader_object, class_loader_load_class, class_name_object)); + + if (env->ExceptionCheck()) { + env->ExceptionClear(); + class_object = nullptr; + } + env->DeleteLocalRef(class_name_object); + env->DeleteLocalRef(class_loader_object); + } + return class_object; +} + +// Vars that we need available for appending text to the log window: +class LoggingUtilsData { + public: + LoggingUtilsData() + : logging_utils_class_(nullptr), + logging_utils_add_log_text_(0), + logging_utils_init_log_window_(0) {} + + ~LoggingUtilsData() { + JNIEnv* env = GetJniEnv(); + assert(env); + if (logging_utils_class_) { + env->DeleteGlobalRef(logging_utils_class_); + } + } + + void Init() { + JNIEnv* env = GetJniEnv(); + assert(env); + + jclass logging_utils_class = FindClass( + env, GetActivity(), "com/google/firebase/example/LoggingUtils"); + assert(logging_utils_class != 0); + + // Need to store as global references so it don't get moved during garbage + // collection. + logging_utils_class_ = + static_cast(env->NewGlobalRef(logging_utils_class)); + env->DeleteLocalRef(logging_utils_class); + + logging_utils_init_log_window_ = env->GetStaticMethodID( + logging_utils_class_, "initLogWindow", "(Landroid/app/Activity;)V"); + logging_utils_add_log_text_ = env->GetStaticMethodID( + logging_utils_class_, "addLogText", "(Ljava/lang/String;)V"); + + env->CallStaticVoidMethod(logging_utils_class_, + logging_utils_init_log_window_, GetActivity()); + } + + void AppendText(const char* text) { + if (logging_utils_class_ == 0) return; // haven't been initted yet + JNIEnv* env = GetJniEnv(); + assert(env); + jstring text_string = env->NewStringUTF(text); + env->CallStaticVoidMethod(logging_utils_class_, logging_utils_add_log_text_, + text_string); + env->DeleteLocalRef(text_string); + } + + private: + jclass logging_utils_class_; + jmethodID logging_utils_add_log_text_; + jmethodID logging_utils_init_log_window_; +}; + +LoggingUtilsData* g_logging_utils_data; + +// Checks if a JNI exception has happened, and if so, logs it to the console. +void CheckJNIException() { + JNIEnv* env = GetJniEnv(); + if (env->ExceptionCheck()) { + // Get the exception text. + jthrowable exception = env->ExceptionOccurred(); + env->ExceptionClear(); + + // Convert the exception to a string. + jclass object_class = env->FindClass("java/lang/Object"); + jmethodID toString = + env->GetMethodID(object_class, "toString", "()Ljava/lang/String;"); + jstring s = (jstring)env->CallObjectMethod(exception, toString); + const char* exception_text = env->GetStringUTFChars(s, nullptr); + + // Log the exception text. + __android_log_print(ANDROID_LOG_INFO, FIREBASE_TESTAPP_NAME, + "-------------------JNI exception:"); + __android_log_print(ANDROID_LOG_INFO, FIREBASE_TESTAPP_NAME, "%s", + exception_text); + __android_log_print(ANDROID_LOG_INFO, FIREBASE_TESTAPP_NAME, + "-------------------"); + + // Also, assert fail. + assert(false); + + // In the event we didn't assert fail, clean up. + env->ReleaseStringUTFChars(s, exception_text); + env->DeleteLocalRef(s); + env->DeleteLocalRef(exception); + } +} + +// Log a message that can be viewed in "adb logcat". +void LogMessage(const char* format, ...) { + static const int kLineBufferSize = 100; + char buffer[kLineBufferSize + 2]; + + va_list list; + va_start(list, format); + int string_len = vsnprintf(buffer, kLineBufferSize, format, list); + string_len = string_len < kLineBufferSize ? string_len : kLineBufferSize; + // append a linebreak to the buffer: + buffer[string_len] = '\n'; + buffer[string_len + 1] = '\0'; + + __android_log_vprint(ANDROID_LOG_INFO, FIREBASE_TESTAPP_NAME, format, list); + g_logging_utils_data->AppendText(buffer); + CheckJNIException(); + va_end(list); +} + +// Get the JNI environment. +JNIEnv* GetJniEnv() { + JavaVM* vm = g_app_state->activity->vm; + JNIEnv* env; + jint result = vm->AttachCurrentThread(&env, nullptr); + return result == JNI_OK ? env : nullptr; +} + +// Execute common_main(), flush pending events and finish the activity. +extern "C" void android_main(struct android_app* state) { + // native_app_glue spawns a new thread, calling android_main() when the + // activity onStart() or onRestart() methods are called. This code handles + // the case where we're re-entering this method on a different thread by + // signalling the existing thread to exit, waiting for it to complete before + // reinitializing the application. + if (g_started) { + g_restarted = true; + // Wait for the existing thread to exit. + pthread_mutex_lock(&g_started_mutex); + pthread_mutex_unlock(&g_started_mutex); + } else { + g_started_mutex = PTHREAD_MUTEX_INITIALIZER; + } + pthread_mutex_lock(&g_started_mutex); + g_started = true; + + // Save native app glue state and setup a callback to track the state. + g_destroy_requested = false; + g_app_state = state; + g_app_state->onAppCmd = OnAppCmd; + + // Create the logging display. + g_logging_utils_data = new LoggingUtilsData(); + g_logging_utils_data->Init(); + + // Execute cross platform entry point. + static const char* argv[] = {FIREBASE_TESTAPP_NAME}; + int return_value = common_main(1, argv); + (void)return_value; // Ignore the return value. + ProcessEvents(10); + + // Clean up logging display. + delete g_logging_utils_data; + g_logging_utils_data = nullptr; + + // Finish the activity. + if (!g_restarted) ANativeActivity_finish(state->activity); + + g_app_state->activity->vm->DetachCurrentThread(); + g_started = false; + g_restarted = false; + pthread_mutex_unlock(&g_started_mutex); +} diff --git a/functions/testapp/src/android/java/com/google/firebase/example/LoggingUtils.java b/functions/testapp/src/android/java/com/google/firebase/example/LoggingUtils.java new file mode 100644 index 00000000..11d67c5b --- /dev/null +++ b/functions/testapp/src/android/java/com/google/firebase/example/LoggingUtils.java @@ -0,0 +1,55 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.example; + +import android.app.Activity; +import android.os.Handler; +import android.os.Looper; +import android.view.Window; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +/** + * A utility class, encapsulating the data and methods required to log arbitrary + * text to the screen, via a non-editable TextView. + */ +public class LoggingUtils { + public static TextView sTextView = null; + + public static void initLogWindow(Activity activity) { + LinearLayout linearLayout = new LinearLayout(activity); + ScrollView scrollView = new ScrollView(activity); + TextView textView = new TextView(activity); + textView.setTag("Logger"); + linearLayout.addView(scrollView); + scrollView.addView(textView); + Window window = activity.getWindow(); + window.takeSurface(null); + window.setContentView(linearLayout); + sTextView = textView; + } + + public static void addLogText(final String text) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (sTextView != null) { + sTextView.append(text); + } + } + }); + } +} diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc new file mode 100644 index 00000000..7d585c71 --- /dev/null +++ b/functions/testapp/src/common_main.cc @@ -0,0 +1,1054 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// 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. + +#include +#include +#include "firebase/app.h" +#include "firebase/auth.h" +#include "firebase/database.h" +#include "firebase/future.h" +#include "firebase/util.h" + +// Thin OS abstraction layer. +#include "main.h" // NOLINT + +// An example of a ValueListener object. This specific version will +// simply log every value it sees, and store them in a list so we can +// confirm that all values were received. +class SampleValueListener : public firebase::database::ValueListener { + public: + void OnValueChanged( + const firebase::database::DataSnapshot& snapshot) override { + LogMessage(" ValueListener.OnValueChanged(%s)", + snapshot.value().AsString().string_value()); + last_seen_value_ = snapshot.value(); + seen_values_.push_back(snapshot.value()); + } + void OnCancelled(const firebase::database::Error& error_code, + const char* error_message) override { + LogMessage("ERROR: SampleValueListener canceled: %d: %s", error_code, + error_message); + } + const firebase::Variant& last_seen_value() { return last_seen_value_; } + bool seen_value(const firebase::Variant& value) { + return std::find(seen_values_.begin(), seen_values_.end(), value) != + seen_values_.end(); + } + size_t num_seen_values() { return seen_values_.size(); } + + private: + firebase::Variant last_seen_value_; + std::vector seen_values_; +}; + +// An example ChildListener class. TODO(jsimantov) implement. +class SampleChildListener : public firebase::database::ChildListener { + public: + void OnChildAdded(const firebase::database::DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildAdded(%s)", snapshot.key()); + events_.push_back(std::string("added ") + snapshot.key()); + } + void OnChildChanged(const firebase::database::DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildChanged(%s)", snapshot.key()); + events_.push_back(std::string("changed ") + snapshot.key()); + } + void OnChildMoved(const firebase::database::DataSnapshot& snapshot, + const char* previous_sibling) override { + LogMessage(" ChildListener.OnChildMoved(%s)", snapshot.key()); + events_.push_back(std::string("moved ") + snapshot.key()); + } + void OnChildRemoved( + const firebase::database::DataSnapshot& snapshot) override { + LogMessage(" ChildListener.OnChildRemoved(%s)", snapshot.key()); + events_.push_back(std::string("removed ") + snapshot.key()); + } + void OnCancelled(const firebase::database::Error& error_code, + const char* error_message) override { + LogMessage("ERROR: SampleChildListener canceled: %d: %s", error_code, + error_message); + } + + // Get the total number of Child events this listener saw. + size_t total_events() { return events_.size(); } + + // Get the number of times this event was seen. + int num_events(const std::string& event) { + int count = 0; + for (int i = 0; i < events_.size(); i++) { + if (events_[i] == event) { + count++; + } + } + return count; + } + + public: + // Vector of strings defining the events we saw, in order. + std::vector events_; +}; + +// A ValueListener that expects a specific value to be set. +class ExpectValueListener : public firebase::database::ValueListener { + public: + explicit ExpectValueListener(firebase::Variant wait_value) + : wait_value_(wait_value.AsString()), got_value_(false) {} + void OnValueChanged( + const firebase::database::DataSnapshot& snapshot) override { + if (snapshot.value().AsString() == wait_value_) { + got_value_ = true; + } else { + LogMessage( + "FAILURE: ExpectValueListener did not receive the expected result."); + } + } + void OnCancelled(const firebase::database::Error& error_code, + const char* error_message) override { + LogMessage("ERROR: ExpectValueListener canceled: %d: %s", error_code, + error_message); + } + + bool got_value() { return got_value_; } + + private: + firebase::Variant wait_value_; + bool got_value_; +}; + +// Wait for a Future to be completed. If the Future returns an error, it will +// be logged. +void WaitForCompletion(const firebase::FutureBase& future, const char* name) { + while (future.status() == firebase::kFutureStatusPending) { + ProcessEvents(100); + } + if (future.status() != firebase::kFutureStatusComplete) { + LogMessage("ERROR: %s returned an invalid result.", name); + } else if (future.error() != 0) { + LogMessage("ERROR: %s returned error %d: %s", name, future.error(), + future.error_message()); + } +} + +extern "C" int common_main(int argc, const char* argv[]) { + ::firebase::App* app; + +#if defined(__ANDROID__) + app = ::firebase::App::Create(GetJniEnv(), GetActivity()); +#else + app = ::firebase::App::Create(); +#endif // defined(__ANDROID__) + + LogMessage("Initialized Firebase App."); + + LogMessage("Initialize Firebase Auth and Firebase Database."); + + // Use ModuleInitializer to initialize both Auth and Database, ensuring no + // dependencies are missing. + ::firebase::database::Database* database = nullptr; + ::firebase::auth::Auth* auth = nullptr; + void* initialize_targets[] = {&auth, &database}; + + const firebase::ModuleInitializer::InitializerFn initializers[] = { + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Auth."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = + ::firebase::auth::Auth::GetAuth(app, &result); + return result; + }, + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Database."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::database::Database**>(targets[1]) = + ::firebase::database::Database::GetInstance(app, &result); + return result; + }}; + + ::firebase::ModuleInitializer initializer; + initializer.Initialize(app, initialize_targets, initializers, + sizeof(initializers) / sizeof(initializers[0])); + + WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); + + if (initializer.InitializeLastResult().error() != 0) { + LogMessage("Failed to initialize Firebase libraries: %s", + initializer.InitializeLastResult().error_message()); + ProcessEvents(2000); + return 1; + } + LogMessage("Successfully initialized Firebase Auth and Firebase Database."); + + database->set_persistence_enabled(true); + + // Sign in using Auth before accessing the database. + // The default Database permissions allow anonymous users access. This will + // work as long as your project's Authentication permissions allow anonymous + // signin. + { + firebase::Future sign_in_future = + auth->SignInAnonymously(); + WaitForCompletion(sign_in_future, "SignInAnonymously"); + if (sign_in_future.error() == firebase::auth::kAuthErrorNone) { + LogMessage("Auth: Signed in anonymously."); + } else { + LogMessage("ERROR: Could not sign in anonymously. Error %d: %s", + sign_in_future.error(), sign_in_future.error_message()); + LogMessage( + " Ensure your application has the Anonymous sign-in provider " + "enabled in Firebase Console."); + LogMessage( + " Attempting to connect to the database anyway. This may fail " + "depending on the security settings."); + } + } + + std::string saved_url; // persists across connections + + // Create a unique child in the database that we can run our tests in. + firebase::database::DatabaseReference ref; + ref = database->GetReference("test_app_data").PushChild(); + + saved_url = ref.url(); + LogMessage("URL: %s", saved_url.c_str()); + + // Set and Get some simple fields. This will set a string, integer, double, + // bool, and current timestamp, and then read them back from the database to + // confirm that they were set. Then it will remove the string value. + { + const char* kSimpleString = "Some simple string"; + const int kSimpleInt = 2; + const int kSimplePriority = 100; + const double kSimpleDouble = 3.4; + const bool kSimpleBool = true; + + { + LogMessage("TEST: Set simple values."); + firebase::Future f1 = + ref.Child("Simple").Child("String").SetValue(kSimpleString); + firebase::Future f2 = + ref.Child("Simple").Child("Int").SetValue(kSimpleInt); + firebase::Future f3 = + ref.Child("Simple").Child("Double").SetValue(kSimpleDouble); + firebase::Future f4 = + ref.Child("Simple").Child("Bool").SetValue(kSimpleBool); + firebase::Future f5 = + ref.Child("Simple") + .Child("Timestamp") + .SetValue(firebase::database::ServerTimestamp()); + firebase::Future f6 = + ref.Child("Simple") + .Child("IntAndPriority") + .SetValueAndPriority(kSimpleInt, kSimplePriority); + WaitForCompletion(f1, "SetSimpleString"); + WaitForCompletion(f2, "SetSimpleInt"); + WaitForCompletion(f3, "SetSimpleDouble"); + WaitForCompletion(f4, "SetSimpleBool"); + WaitForCompletion(f5, "SetSimpleTimestamp"); + WaitForCompletion(f6, "SetSimpleIntAndPriority"); + if (f1.error() != firebase::database::kErrorNone || + f2.error() != firebase::database::kErrorNone || + f3.error() != firebase::database::kErrorNone || + f4.error() != firebase::database::kErrorNone || + f5.error() != firebase::database::kErrorNone || + f6.error() != firebase::database::kErrorNone) { + LogMessage("ERROR: Set simple values failed."); + LogMessage(" String: Error %d: %s", f1.error(), f1.error_message()); + LogMessage(" Int: Error %d: %s", f2.error(), f2.error_message()); + LogMessage(" Double: Error %d: %s", f3.error(), f3.error_message()); + LogMessage(" Bool: Error %d: %s", f4.error(), f4.error_message()); + LogMessage(" Timestamp: Error %d: %s", f5.error(), f5.error_message()); + LogMessage(" Int and Priority: Error %d: %s", f6.error(), + f6.error_message()); + } else { + LogMessage("SUCCESS: Set simple values."); + } + } + // Get the values that we just set, and confirm that they match what we + // set them to. + { + LogMessage("TEST: Get simple values."); + firebase::Future f1 = + ref.Child("Simple").Child("String").GetValue(); + firebase::Future f2 = + ref.Child("Simple").Child("Int").GetValue(); + firebase::Future f3 = + ref.Child("Simple").Child("Double").GetValue(); + firebase::Future f4 = + ref.Child("Simple").Child("Bool").GetValue(); + firebase::Future f5 = + ref.Child("Simple").Child("Timestamp").GetValue(); + firebase::Future f6 = + ref.Child("Simple").Child("IntAndPriority").GetValue(); + WaitForCompletion(f1, "GetSimpleString"); + WaitForCompletion(f2, "GetSimpleInt"); + WaitForCompletion(f3, "GetSimpleDouble"); + WaitForCompletion(f4, "GetSimpleBool"); + WaitForCompletion(f5, "GetSimpleTimestamp"); + WaitForCompletion(f6, "GetSimpleIntAndPriority"); + + if (f1.error() == firebase::database::kErrorNone && + f2.error() == firebase::database::kErrorNone && + f3.error() == firebase::database::kErrorNone && + f4.error() == firebase::database::kErrorNone && + f5.error() == firebase::database::kErrorNone && + f6.error() == firebase::database::kErrorNone) { + // Get the current time to compare to the Timestamp. + int64_t current_time_milliseconds = + static_cast(time(nullptr)) * 1000L; + int64_t time_difference = f5.result()->value().AsInt64().int64_value() - + current_time_milliseconds; + // As long as our timestamp is within a day, it's correct enough for our + // purposes. + const int64_t kAllowedTimeDifferenceMilliseconds = + 1000L * 60L * 60L * 24L; + + if (f1.result()->value().AsString() != kSimpleString || + f2.result()->value().AsInt64() != kSimpleInt || + f3.result()->value().AsDouble() != kSimpleDouble || + f4.result()->value().AsBool() != kSimpleBool || + f6.result()->value().AsInt64() != kSimpleInt || + f6.result()->priority().AsInt64() != kSimplePriority || + time_difference > kAllowedTimeDifferenceMilliseconds || + time_difference < -kAllowedTimeDifferenceMilliseconds) { + LogMessage("ERROR: Get simple values failed, values did not match."); + LogMessage(" String: Got \"%s\", expected \"%s\"", + f1.result()->value().string_value(), kSimpleString); + LogMessage(" Int: Got %lld, expected %d", + f2.result()->value().AsInt64().int64_value(), kSimpleInt); + LogMessage(" Double: Got %lf, expected %lf", + f3.result()->value().AsDouble().double_value(), + kSimpleDouble); + LogMessage( + " Bool: Got %s, expected %s", + f4.result()->value().AsBool().bool_value() ? "true" : "false", + kSimpleBool ? "true" : "false"); + LogMessage(" Timestamp: Got %lld, expected something near %lld", + f5.result()->value().AsInt64().int64_value(), + current_time_milliseconds); + LogMessage( + " IntAndPriority: Got {.value:%lld,.priority:%lld}, expected " + "{.value:%d,.priority:%d}", + f6.result()->value().AsInt64().int64_value(), + f6.result()->priority().AsInt64().int64_value(), kSimpleInt, + kSimplePriority); + + } else { + LogMessage("SUCCESS: Get simple values."); + } + } else { + LogMessage("ERROR: Get simple values failed."); + } + + // Try removing one value. + { + LogMessage("TEST: Removing a value."); + WaitForCompletion(ref.Child("Simple").Child("String").RemoveValue(), + "RemoveSimpleString"); + firebase::Future future = + ref.Child("Simple").Child("String").GetValue(); + WaitForCompletion(future, "GetRemovedSimpleString"); + if (future.error() == firebase::database::kErrorNone && + future.result()->value().is_null()) { + LogMessage("SUCCESS: Value was removed."); + } else { + LogMessage("ERROR: Value was not removed."); + } + } + } + } + +#if defined(__ANDROID__) || TARGET_OS_IPHONE + // Actually shut down the realtime database, and restart it, to make sure + // that persistence persists across database object instances. + { + // Write a value that we can test for. + const char* kPersistenceString = "Persistence Test!"; + WaitForCompletion(ref.Child("PersistenceTest").SetValue(kPersistenceString), + "SetPersistenceTestValue"); + + LogMessage("Destroying database object."); + delete database; + LogMessage("Recreating database object."); + database = ::firebase::database::Database::GetInstance(app); + + // Offline mode. If persistence works, we should still be able to fetch + // our value even though we're offline. + + database->GoOffline(); + ref = database->GetReferenceFromUrl(saved_url.c_str()); + + { + LogMessage( + "TEST: Fetching the value while offline via AddValueListener."); + ExpectValueListener* listener = + new ExpectValueListener(kPersistenceString); + ref.Child("PersistenceTest").AddValueListener(listener); + + while (!listener->got_value()) { + ProcessEvents(100); + } + delete listener; + listener = nullptr; + } + + { + LogMessage("TEST: Fetching the value while offline via GetValue."); + firebase::Future value_future = + ref.Child("PersistenceTest").GetValue(); + + WaitForCompletion(value_future, "GetValue"); + + const firebase::database::DataSnapshot& result = *value_future.result(); + + if (value_future.error() == firebase::database::kErrorNone) { + if (result.value().AsString() == kPersistenceString) { + LogMessage("SUCCESS: GetValue returned the correct value."); + } else { + LogMessage("FAILURE: GetValue returned an incorrect value."); + } + } else { + LogMessage("FAILURE: GetValue Future returned an error."); + } + } + + LogMessage("Going back online."); + database->GoOnline(); + } +#endif // defined(__ANDROID__) || TARGET_OS_IPHONE + + // Test running a transaction. This will call RunTransaction and set + // some values, including incrementing the player's score. + { + firebase::Future transaction_future; + static const int kInitialScore = 500; + static const int kAddedScore = 100; + LogMessage("TEST: Run transaction."); + // Set an initial score of 500 points. + WaitForCompletion(ref.Child("TransactionResult") + .Child("player_score") + .SetValue(kInitialScore), + "SetInitialScoreValue"); + // The transaction will set the player's item and class, and increment + // their score by 100 points. + int score_delta = 100; + transaction_future = + ref.Child("TransactionResult") + .RunTransaction( + [](firebase::database::MutableData* data, + void* score_delta_void) { + LogMessage(" Transaction function executing."); + data->Child("player_item").set_value("Fire sword"); + data->Child("player_class").set_value("Warrior"); + // Increment the current score by 100. + int64_t score = data->Child("player_score") + .value() + .AsInt64() + .int64_value(); + data->Child("player_score") + .set_value(score + + *reinterpret_cast(score_delta_void)); + return firebase::database::kTransactionResultSuccess; + }, + &score_delta); + WaitForCompletion(transaction_future, "RunTransaction"); + + // Check whether the transaction succeeded, was aborted, or failed with an + // error. + if (transaction_future.error() == firebase::database::kErrorNone) { + LogMessage("SUCCESS: Transaction committed."); + } else if (transaction_future.error() == + firebase::database::kErrorTransactionAbortedByUser) { + LogMessage("ERROR: Transaction was aborted."); + } else { + LogMessage("ERROR: Transaction returned error %d: %s", + transaction_future.error(), + transaction_future.error_message()); + } + + // If the transaction succeeded, let's read back the values that were + // written to confirm they match. + if (transaction_future.error() == firebase::database::kErrorNone) { + LogMessage("TEST: Test reading transaction results."); + + firebase::Future read_future = + ref.Child("TransactionResult").GetValue(); + WaitForCompletion(read_future, "ReadTransactionResults"); + if (read_future.error() != firebase::database::kErrorNone) { + LogMessage("ERROR: Error %d reading transaction results: %s", + read_future.error(), read_future.error_message()); + } else { + const firebase::database::DataSnapshot& result = *read_future.result(); + if (result.children_count() == 3 && result.HasChild("player_item") && + result.Child("player_item").value() == "Fire sword" && + result.HasChild("player_class") && + result.Child("player_class").value() == "Warrior" && + result.HasChild("player_score") && + result.Child("player_score").value().AsInt64() == + kInitialScore + kAddedScore) { + if (result.value() != transaction_future.result()->value()) { + LogMessage( + "ERROR: Transaction snapshot did not match newly read data."); + } else { + LogMessage("SUCCESS: Transaction test succeeded."); + } + } else { + LogMessage("ERROR: Transaction result was incorrect."); + } + } + } + } + + // Set up a map of values that we will put into the database, then modify. + std::map sample_values; + sample_values.insert(std::make_pair("Apple", 1)); + sample_values.insert(std::make_pair("Banana", 2)); + sample_values.insert(std::make_pair("Cranberry", 3)); + sample_values.insert(std::make_pair("Durian", 4)); + sample_values.insert(std::make_pair("Eggplant", 5)); + + // Run UpdateChildren, specifying some existing children (which will be + // modified), some children with a value of null (which will be removed), + // and some new children (which will be added). + { + LogMessage("TEST: UpdateChildren."); + + WaitForCompletion(ref.Child("UpdateChildren").SetValue(sample_values), + "UpdateSetValues"); + + // Set each key's value to what's given in this map. We use a map of + // Variant so that we can specify Variant::Null() to remove a key from the + // database. + std::map update_values; + update_values.insert(std::make_pair("Apple", 100)); + update_values.insert(std::make_pair("Durian", "is a fruit!")); + update_values.insert(std::make_pair("Eggplant", firebase::Variant::Null())); + update_values.insert(std::make_pair("Fig", 6)); + + WaitForCompletion(ref.Child("UpdateChildren").UpdateChildren(update_values), + "UpdateChildren"); + + // Get the values that were written to ensure they were updated properly. + firebase::Future updated_values = + ref.Child("UpdateChildren").GetValue(); + WaitForCompletion(updated_values, "UpdateChildrenResult"); + if (updated_values.error() == firebase::database::kErrorNone) { + const firebase::database::DataSnapshot& result = *updated_values.result(); + bool failed = false; + if (result.children_count() != 5) { + LogMessage( + "ERROR: UpdateChildren returned an unexpected number of " + "children: " + "%d", + result.children_count()); + failed = true; + } + if (!result.HasChild("Apple") || + result.Child("Apple").value().AsInt64() != 100) { + LogMessage("ERROR: Child key 'Apple' was not updated correctly."); + failed = true; + } + if (!result.HasChild("Banana") || + result.Child("Banana").value().AsInt64() != 2) { + LogMessage("ERROR: Child key 'Banana' was not updated correctly."); + failed = true; + } + if (!result.HasChild("Cranberry") || + result.Child("Cranberry").value().AsInt64() != 3) { + LogMessage("ERROR: Child key 'Cranberry' was not updated correctly."); + failed = true; + } + if (!result.HasChild("Durian") || + result.Child("Durian").value().AsString() != "is a fruit!") { + LogMessage("ERROR: Child key 'Durian' was not updated correctly."); + failed = true; + } + if (result.HasChild("Eggplant")) { + LogMessage("ERROR: Child key 'Eggplant' was not removed."); + failed = true; + } + if (!result.HasChild("Fig") || + result.Child("Fig").value().AsInt64() != 6) { + LogMessage("ERROR: Child key 'Fig' was not added correctly."); + failed = true; + } + if (!failed) { + LogMessage("SUCCESS: UpdateChildren succeeded."); + } else { + LogMessage( + "ERROR: UpdateChildren did not modify the children as expected."); + } + } else { + LogMessage("ERROR: Couldn't retrieve updated values."); + } + } + + // Test Query, which gives you different views into the same location in the + // database. + { + LogMessage("TEST: Query filtering."); + + firebase::Future set_future = + ref.Child("QueryFiltering").SetValue(sample_values); + WaitForCompletion(set_future, "QuerySetValues"); + // Create a query for keys in the lexicographical range "B" to "Dz". + auto b_to_d = ref.Child("QueryFiltering") + .OrderByKey() + .StartAt("B") + .EndAt("Dz") + .GetValue(); + // Create a query for values in the numeric range 1 to 3. + auto one_to_three = ref.Child("QueryFiltering") + .OrderByValue() + .StartAt(1) + .EndAt(3) + .GetValue(); + // Create a query ordered by value, but limited to only the highest two + // values. + auto four_and_five = + ref.Child("QueryFiltering").OrderByValue().LimitToLast(2).GetValue(); + // Create a query ordered by key, but limited to only the lowest two keys. + auto a_and_b = + ref.Child("QueryFiltering").OrderByKey().LimitToFirst(2).GetValue(); + // Create a query limited only to the key "Cranberry". + auto c_only = ref.Child("QueryFiltering") + .OrderByKey() + .EqualTo("Cranberry") + .GetValue(); + + WaitForCompletion(b_to_d, "QueryBthruD"); + WaitForCompletion(one_to_three, "Query1to3"); + WaitForCompletion(four_and_five, "Query4and5"); + WaitForCompletion(a_and_b, "QueryAandB"); + WaitForCompletion(c_only, "QueryC"); + + bool failed = false; + // Check that the queries each returned the expected results. + if (b_to_d.error() != firebase::database::kErrorNone || + b_to_d.result()->children_count() != 3 || + !b_to_d.result()->HasChild("Banana") || + !b_to_d.result()->HasChild("Cranberry") || + !b_to_d.result()->HasChild("Durian")) { + LogMessage("ERROR: Query B-to-D returned unexpected results."); + failed = true; + } + if (one_to_three.error() != firebase::database::kErrorNone || + one_to_three.result()->children_count() != 3 || + !one_to_three.result()->HasChild("Apple") || + !one_to_three.result()->HasChild("Banana") || + !one_to_three.result()->HasChild("Cranberry")) { + LogMessage("ERROR: Query 1-to-3 returned unexpected results."); + failed = true; + } + if (four_and_five.error() != firebase::database::kErrorNone || + four_and_five.result()->children_count() != 2 || + !four_and_five.result()->HasChild("Durian") || + !four_and_five.result()->HasChild("Eggplant")) { + LogMessage("ERROR: Query 4-and-5 returned unexpected results."); + failed = true; + } + if (a_and_b.error() != firebase::database::kErrorNone || + a_and_b.result()->children_count() != 2 || + !a_and_b.result()->HasChild("Apple") || + !a_and_b.result()->HasChild("Banana")) { + LogMessage("ERROR: Query A-and-B returned unexpected results."); + failed = true; + } + if (c_only.error() != firebase::database::kErrorNone || + c_only.result()->children_count() != 1 || + !c_only.result()->HasChild("Cranberry")) { + LogMessage("ERROR: Query C-only returned unexpected results."); + failed = true; + } + if (!failed) { + LogMessage("SUCCESS: Query filtering succeeded."); + } + } + + // Test a ValueListener, which sits on a Query and listens for changes in + // the value at that location. + { + LogMessage("TEST: ValueListener"); + SampleValueListener* listener = new SampleValueListener(); + WaitForCompletion(ref.Child("ValueListener").SetValue(0), "SetValueZero"); + // Attach the listener, then set 3 values, which will trigger the + // listener. + ref.Child("ValueListener").AddValueListener(listener); + + // The listener's OnChanged callback is triggered once when the listener is + // attached and again every time the data, including children, changes. + // Wait for here for a moment for the initial values to be received. + ProcessEvents(2000); + + WaitForCompletion(ref.Child("ValueListener").SetValue(1), "SetValueOne"); + WaitForCompletion(ref.Child("ValueListener").SetValue(2), "SetValueTwo"); + WaitForCompletion(ref.Child("ValueListener").SetValue(3), "SetValueThree"); + + LogMessage(" Waiting for ValueListener..."); + + // Wait a few seconds for the value listener to be triggered. + ProcessEvents(2000); + + // Unregister the listener, so it stops triggering. + ref.Child("ValueListener").RemoveValueListener(listener); + + // Ensure that the listener is not triggered once removed. + WaitForCompletion(ref.Child("ValueListener").SetValue(4), "SetValueFour"); + + // Wait a few more seconds to ensure the listener is not triggered. + ProcessEvents(2000); + + // Ensure that the listener was only triggered 4 times, with the values + // 0 (the initial value), 1, 2, and 3. + if (listener->num_seen_values() == 4 && listener->seen_value(0) && + listener->seen_value(1) && listener->seen_value(2) && + listener->seen_value(3)) { + LogMessage("SUCCESS: ValueListener got all values."); + } else { + LogMessage("ERROR: ValueListener did not get all values."); + } + + delete listener; + } + // Test a ChildListener, which sits on a Query and listens for changes in + // the child hierarchy at the location. + { + LogMessage("TEST: ChildListener"); + SampleChildListener* listener = new SampleChildListener(); + + // Set a child listener that only listens for entities of type "enemy". + auto entity_list = ref.Child("ChildListener").Child("entity_list"); + + entity_list.OrderByChild("entity_type") + .EqualTo("enemy") + .AddChildListener(listener); + + // The listener's OnChanged callback is triggered once when the listener is + // attached and again every time the data, including children, changes. + // Wait for here for a moment for the initial values to be received. + ProcessEvents(2000); + + std::map params; + params["entity_name"] = "cobra"; + params["entity_type"] = "enemy"; + WaitForCompletion(entity_list.Child("0").SetValueAndPriority(params, 0), + "SetEntity0"); + params["entity_name"] = "warrior"; + params["entity_type"] = "hero"; + WaitForCompletion(entity_list.Child("1").SetValueAndPriority(params, 10), + "SetEntity1"); + params["entity_name"] = "wizard"; + params["entity_type"] = "hero"; + WaitForCompletion(entity_list.Child("2").SetValueAndPriority(params, 20), + "SetEntity2"); + params["entity_name"] = "rat"; + params["entity_type"] = "enemy"; + WaitForCompletion(entity_list.Child("3").SetValueAndPriority(params, 30), + "SetEntity3"); + params["entity_name"] = "thief"; + params["entity_type"] = "enemy"; + WaitForCompletion(entity_list.Child("4").SetValueAndPriority(params, 40), + "SetEntity4"); + params["entity_name"] = "paladin"; + params["entity_type"] = "hero"; + WaitForCompletion(entity_list.Child("5").SetValueAndPriority(params, 50), + "SetEntity5"); + params["entity_name"] = "ghost"; + params["entity_type"] = "enemy"; + WaitForCompletion(entity_list.Child("6").SetValueAndPriority(params, 60), + "SetEntity6"); + params["entity_name"] = "dragon"; + params["entity_type"] = "enemy"; + WaitForCompletion(entity_list.Child("7").SetValueAndPriority(params, 70), + "SetEntity7"); + // Now the thief becomes a hero! + WaitForCompletion( + entity_list.Child("4").Child("entity_type").SetValue("hero"), + "SetEntity4Type"); + // Now the dragon becomes a super-dragon! + WaitForCompletion( + entity_list.Child("7").Child("entity_name").SetValue("super-dragon"), + "SetEntity7Name"); + // Now the super-dragon becomes an mega-dragon! + WaitForCompletion( + entity_list.Child("7").Child("entity_name").SetValue("mega-dragon"), + "SetEntity7NameAgain"); + // And now we change a hero entity, which the Query ignores. + WaitForCompletion( + entity_list.Child("2").Child("entity_name").SetValue("super-wizard"), + "SetEntity2Value"); + // Now poof, the mega-dragon is gone. + WaitForCompletion(entity_list.Child("7").RemoveValue(), "RemoveEntity7"); + + LogMessage(" Waiting for ChildListener..."); + + // Wait a few seconds for the child listener to be triggered. + ProcessEvents(2000); + + // Unregister the listener, so it stops triggering. + entity_list.OrderByChild("entity_type") + .EqualTo("enemy") + .RemoveChildListener(listener); + + // Wait a few seconds for the child listener to be triggered. + ProcessEvents(2000); + + // Make one more change, to ensure the listener has been removed. + WaitForCompletion(entity_list.Child("6").SetPriority(0), + "SetEntity6Priority"); + + // We are expecting to have the following events: + bool failed = false; + if (listener->num_events("added 0") != 1) { + LogMessage( + "ERROR: OnChildAdded(0) was called an incorrect number of times."); + failed = true; + } + if (listener->num_events("added 3") != 1) { + LogMessage( + "ERROR: OnChildAdded(3) was called an incorrect number of times."); + failed = true; + } + if (listener->num_events("added 4") != 1) { + LogMessage( + "ERROR: OnChildAdded(4) was called an incorrect number of times."); + failed = true; + } + if (listener->num_events("added 6") != 1) { + LogMessage( + "ERROR: OnChildAdded(6) was called an incorrect number of times."); + failed = true; + } + if (listener->num_events("added 7") != 1) { + LogMessage( + "ERROR: OnChildAdded(7) was called an incorrect number of times."); + failed = true; + } + if (listener->num_events("removed 4") != 1) { + LogMessage( + "ERROR: OnChildRemoved(4) was called an incorrect number of " + "times."); + failed = true; + } + if (listener->num_events("changed 7") != 2) { + LogMessage( + "ERROR: OnChildChanged(7) was called an incorrect number of " + "times."); + failed = true; + } + if (listener->num_events("removed 7") != 1) { + LogMessage( + "ERROR: OnChildRemoved(7) was called an incorrect number of " + "times."); + failed = true; + } + if (listener->total_events() != 9) { + LogMessage("ERROR: ChildListener got an incorrect number of events."); + failed = true; + } + if (!failed) { + LogMessage("SUCCESS: ChildListener got all child events."); + } + delete listener; + } + + // Now check OnDisconnect. When you set an OnDisconnect handler for a + // database location, an operation will be performed on that location when + // you disconnect from Firebase Database. In this sample app, we replicate + // this by shutting down Firebase Database, then starting it up again and + // checking to see if the OnDisconnect actions were performed. + { + LogMessage("TEST: OnDisconnect"); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("SetValueTo1") + .OnDisconnect() + ->SetValue(1), + "OnDisconnectSetValue1"); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("SetValue2Priority3") + .OnDisconnect() + ->SetValueAndPriority(2, 3), + "OnDisconnectSetValue2Priority3"); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("SetValueButThenCancel") + .OnDisconnect() + ->SetValue("Going to cancel this"), + "OnDisconnectSetValueToCancel"); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("SetValueButThenCancel") + .OnDisconnect() + ->Cancel(), + "OnDisconnectCancel"); + // Set a value that we will then remove on disconnect. + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("RemoveValue") + .SetValue("Will be removed"), + "SetValueToRemove"); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("RemoveValue") + .OnDisconnect() + ->RemoveValue(), + "OnDisconnectRemoveValue"); + // Set up a map to pass to OnDisconnect()->UpdateChildren(). + std::map children; + children.insert(std::make_pair("one", 1)); + children.insert(std::make_pair("two", 2)); + children.insert(std::make_pair("three", 3)); + WaitForCompletion(ref.Child("OnDisconnectTests") + .Child("UpdateChildren") + .OnDisconnect() + ->UpdateChildren(children), + "OnDisconnectUpdateChildren"); + LogMessage(" Disconnection handlers registered."); + } + + // Go offline, wait a moment, then go online again. We set up a + // ValueListener + // on one of the OnDisconnect locations we set above, so we can see when the + // disconnection triggers. + { + ExpectValueListener* listener = new ExpectValueListener(1); + ref.Child("OnDisconnectTests") + .Child("SetValueTo1") + .AddValueListener(listener); + + LogMessage(" Disconnecting from Firebase Database."); + database->GoOffline(); + + while (!listener->got_value()) { + ProcessEvents(100); + } + ref.Child("OnDisconnectTests") + .Child("SetValueTo1") + .RemoveValueListener(listener); + delete listener; + listener = nullptr; + + LogMessage(" Reconnecting to Firebase Database."); + database->GoOnline(); + } + + /// Check that the DisconnectionHandler actions were performed. + /// Get a brand new reference to the location to be sure. + ref = database->GetReferenceFromUrl(saved_url.c_str()); + + firebase::Future future = + ref.Child("OnDisconnectTests").GetValue(); + WaitForCompletion(future, "ReadOnDisconnectChanges"); + bool failed = false; + + if (future.error() == firebase::database::kErrorNone) { + const firebase::database::DataSnapshot& result = *future.result(); + if (!result.HasChild("SetValueTo1") || + result.Child("SetValueTo1").value().AsInt64().int64_value() != 1) { + LogMessage("ERROR: OnDisconnect.SetValue(1) failed."); + failed = true; + } + if (!result.HasChild("SetValue2Priority3") || + result.Child("SetValue2Priority3").value().AsInt64().int64_value() != + 2 || + result.Child("SetValue2Priority3").priority().AsInt64().int64_value() != + 3) { + LogMessage("ERROR: OnDisconnect.SetValueAndPriority(2, 3) failed."); + failed = true; + } + if (result.HasChild("RemoveValue")) { + LogMessage("ERROR: OnDisconnect.RemoveValue() failed."); + failed = true; + } + if (result.HasChild("SetValueButThenCancel")) { + LogMessage("ERROR: OnDisconnect.Cancel() failed."); + failed = true; + } + if (!result.HasChild("UpdateChildren") || + !result.Child("UpdateChildren").HasChild("one") || + result.Child("UpdateChildren") + .Child("one") + .value() + .AsInt64() + .int64_value() != 1 || + !result.Child("UpdateChildren").HasChild("two") || + result.Child("UpdateChildren") + .Child("two") + .value() + .AsInt64() + .int64_value() != 2 || + !result.Child("UpdateChildren").HasChild("three") || + result.Child("UpdateChildren") + .Child("three") + .value() + .AsInt64() + .int64_value() != 3) { + LogMessage("ERROR: OnDisconnect.UpdateChildren() failed."); + failed = true; + } + + if (!failed) { + LogMessage("SUCCESS: OnDisconnect values were written properly."); + } + } else { + LogMessage("ERROR: Couldn't read OnDisconnect changes, error %d: %s.", + future.error(), future.error_message()); + } + + bool test_snapshot_was_valid = false; + firebase::database::DataSnapshot* test_snapshot = nullptr; + if (future.error() == firebase::database::kErrorNone) { + // This is a little convoluted as it's not possible to construct an + // empty test snapshot so we copy the result and point at the copy. + static firebase::database::DataSnapshot copied_snapshot = // NOLINT + *future.result(); // NOLINT + test_snapshot = &copied_snapshot; + test_snapshot_was_valid = test_snapshot->is_valid(); + } + + LogMessage("Shutdown the Database library."); + delete database; + database = nullptr; + + // Ensure that the ref we had is now invalid. + if (!ref.is_valid()) { + LogMessage("SUCCESS: Reference was invalidated on library shutdown."); + } else { + LogMessage("ERROR: Reference is still valid after library shutdown."); + } + + if (test_snapshot_was_valid && test_snapshot) { + if (!test_snapshot->is_valid()) { + LogMessage("SUCCESS: Snapshot was invalidated on library shutdown."); + } else { + LogMessage("ERROR: Snapshot is still valid after library shutdown."); + } + } else { + LogMessage( + "WARNING: Snapshot was already invalid at shutdown, couldn't check."); + } + + LogMessage("Signing out from anonymous account."); + auth->SignOut(); + LogMessage("Shutdown the Auth library."); + delete auth; + auth = nullptr; + + LogMessage("Shutdown Firebase App."); + delete app; + + // Wait until the user wants to quit the app. + while (!ProcessEvents(1000)) { + } + + return 0; +} diff --git a/functions/testapp/src/desktop/desktop_main.cc b/functions/testapp/src/desktop/desktop_main.cc new file mode 100644 index 00000000..0220c688 --- /dev/null +++ b/functions/testapp/src/desktop/desktop_main.cc @@ -0,0 +1,125 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// 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. + +#include +#include +#include + +#ifdef _WIN32 +#include +#define chdir _chdir +#else +#include +#endif // _WIN32 + +#ifdef _WIN32 +#include +#endif // _WIN32 + +#include +#include + +#include "main.h" // NOLINT + +// The TO_STRING macro is useful for command line defined strings as the quotes +// get stripped. +#define TO_STRING_EXPAND(X) #X +#define TO_STRING(X) TO_STRING_EXPAND(X) + +// Path to the Firebase config file to load. +#ifdef FIREBASE_CONFIG +#define FIREBASE_CONFIG_STRING TO_STRING(FIREBASE_CONFIG) +#else +#define FIREBASE_CONFIG_STRING "" +#endif // FIREBASE_CONFIG + +extern "C" int common_main(int argc, const char* argv[]); + +static bool quit = false; + +#ifdef _WIN32 +static BOOL WINAPI SignalHandler(DWORD event) { + if (!(event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT)) { + return FALSE; + } + quit = true; + return TRUE; +} +#else +static void SignalHandler(int /* ignored */) { quit = true; } +#endif // _WIN32 + +bool ProcessEvents(int msec) { +#ifdef _WIN32 + Sleep(msec); +#else + usleep(msec * 1000); +#endif // _WIN32 + return quit; +} + +std::string PathForResource() { + return std::string(); +} + +void LogMessage(const char* format, ...) { + va_list list; + va_start(list, format); + vprintf(format, list); + va_end(list); + printf("\n"); + fflush(stdout); +} + +WindowContext GetWindowContext() { return nullptr; } + +// Change the current working directory to the directory containing the +// specified file. +void ChangeToFileDirectory(const char* file_path) { + std::string path(file_path); + std::replace(path.begin(), path.end(), '\\', '/'); + auto slash = path.rfind('/'); + if (slash != std::string::npos) { + std::string directory = path.substr(0, slash); + if (!directory.empty()) chdir(directory.c_str()); + } +} + +int main(int argc, const char* argv[]) { + ChangeToFileDirectory( + FIREBASE_CONFIG_STRING[0] != '\0' ? + FIREBASE_CONFIG_STRING : argv[0]); // NOLINT +#ifdef _WIN32 + SetConsoleCtrlHandler((PHANDLER_ROUTINE)SignalHandler, TRUE); +#else + signal(SIGINT, SignalHandler); +#endif // _WIN32 + return common_main(argc, argv); +} + +#if defined(_WIN32) +// Returns the number of microseconds since the epoch. +int64_t WinGetCurrentTimeInMicroseconds() { + FILETIME file_time; + GetSystemTimeAsFileTime(&file_time); + + ULARGE_INTEGER now; + now.LowPart = file_time.dwLowDateTime; + now.HighPart = file_time.dwHighDateTime; + + // Windows file time is expressed in 100s of nanoseconds. + // To convert to microseconds, multiply x10. + return now.QuadPart * 10LL; +} +#endif diff --git a/functions/testapp/src/ios/ios_main.mm b/functions/testapp/src/ios/ios_main.mm new file mode 100644 index 00000000..2adcac9c --- /dev/null +++ b/functions/testapp/src/ios/ios_main.mm @@ -0,0 +1,120 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#include + +#include "main.h" + +extern "C" int common_main(int argc, const char* argv[]); + +@interface AppDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; + +@end + +@interface FTAViewController : UIViewController + +@end + +static int g_exit_status = 0; +static bool g_shutdown = false; +static NSCondition *g_shutdown_complete; +static NSCondition *g_shutdown_signal; +static UITextView *g_text_view; +static UIView *g_parent_view; + +@implementation FTAViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + g_parent_view = self.view; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + const char *argv[] = {FIREBASE_TESTAPP_NAME}; + [g_shutdown_signal lock]; + g_exit_status = common_main(1, argv); + [g_shutdown_complete signal]; + }); +} + +@end + +bool ProcessEvents(int msec) { + [g_shutdown_signal + waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:static_cast(msec) / 1000.0f]]; + return g_shutdown; +} + +WindowContext GetWindowContext() { + return g_parent_view; +} + +// Log a message that can be viewed in the console. +void LogMessage(const char* format, ...) { + va_list args; + NSString *formatString = @(format); + + va_start(args, format); + NSString *message = [[NSString alloc] initWithFormat:formatString arguments:args]; + va_end(args); + + NSLog(@"%@", message); + message = [message stringByAppendingString:@"\n"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + g_text_view.text = [g_text_view.text stringByAppendingString:message]; + }); +} + +int main(int argc, char* argv[]) { + @autoreleasepool { + UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } + return g_exit_status; +} + +@implementation AppDelegate + +- (BOOL)application:(UIApplication*)application + didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + g_shutdown_complete = [[NSCondition alloc] init]; + g_shutdown_signal = [[NSCondition alloc] init]; + [g_shutdown_complete lock]; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + FTAViewController *viewController = [[FTAViewController alloc] init]; + self.window.rootViewController = viewController; + [self.window makeKeyAndVisible]; + + g_text_view = [[UITextView alloc] initWithFrame:viewController.view.bounds]; + + g_text_view.accessibilityIdentifier = @"Logger"; + g_text_view.editable = NO; + g_text_view.scrollEnabled = YES; + g_text_view.userInteractionEnabled = YES; + + [viewController.view addSubview:g_text_view]; + + return YES; +} + +- (void)applicationWillTerminate:(UIApplication *)application { + g_shutdown = true; + [g_shutdown_signal signal]; + [g_shutdown_complete wait]; +} + +@end diff --git a/functions/testapp/src/main.h b/functions/testapp/src/main.h new file mode 100644 index 00000000..2eda2c10 --- /dev/null +++ b/functions/testapp/src/main.h @@ -0,0 +1,63 @@ +// Copyright 2016 Google Inc. All rights reserved. +// +// 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. + +#ifndef FIREBASE_TESTAPP_MAIN_H_ // NOLINT +#define FIREBASE_TESTAPP_MAIN_H_ // NOLINT + +#if defined(__ANDROID__) +#include +#include +#elif defined(__APPLE__) +extern "C" { +#include +} // extern "C" +#endif // __ANDROID__ + +// Defined using -DANDROID_MAIN_APP_NAME=some_app_name when compiling this +// file. +#ifndef FIREBASE_TESTAPP_NAME +#define FIREBASE_TESTAPP_NAME "android_main" +#endif // FIREBASE_TESTAPP_NAME + +// Cross platform logging method. +// Implemented by android/android_main.cc or ios/ios_main.mm. +extern "C" void LogMessage(const char* format, ...); + +// Platform-independent method to flush pending events for the main thread. +// Returns true when an event requesting program-exit is received. +bool ProcessEvents(int msec); + +// WindowContext represents the handle to the parent window. It's type +// (and usage) vary based on the OS. +#if defined(__ANDROID__) +typedef jobject WindowContext; // A jobject to the Java Activity. +#elif defined(__APPLE__) +typedef id WindowContext; // A pointer to an iOS UIView. +#else +typedef void* WindowContext; // A void* for any other environments. +#endif + +#if defined(__ANDROID__) +// Get the JNI environment. +JNIEnv* GetJniEnv(); +// Get the activity. +jobject GetActivity(); +#endif // defined(__ANDROID__) + +// Returns a variable that describes the window context for the app. On Android +// this will be a jobject pointing to the Activity. On iOS, it's an id pointing +// to the root view of the view controller. +WindowContext GetWindowContext(); + +#endif // FIREBASE_TESTAPP_MAIN_H_ // NOLINT diff --git a/functions/testapp/testapp.xcodeproj/project.pbxproj b/functions/testapp/testapp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..baf45c4c --- /dev/null +++ b/functions/testapp/testapp.xcodeproj/project.pbxproj @@ -0,0 +1,312 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 520BC0391C869159008CFBC3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */; }; + 529226D61C85F68000C89379 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D51C85F68000C89379 /* Foundation.framework */; }; + 529226D81C85F68000C89379 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D71C85F68000C89379 /* CoreGraphics.framework */; }; + 529226DA1C85F68000C89379 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D91C85F68000C89379 /* UIKit.framework */; }; + 529227211C85FB6A00C89379 /* common_main.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5292271F1C85FB6A00C89379 /* common_main.cc */; }; + 529227241C85FB7600C89379 /* ios_main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 529227221C85FB7600C89379 /* ios_main.mm */; }; + 52B71EBB1C8600B600398745 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52B71EBA1C8600B600398745 /* Images.xcassets */; }; + D66B16871CE46E8900E5638A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 529226D21C85F68000C89379 /* testapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = testapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 529226D51C85F68000C89379 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 529226D71C85F68000C89379 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 529226D91C85F68000C89379 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 529226EE1C85F68000C89379 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 5292271F1C85FB6A00C89379 /* common_main.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = common_main.cc; path = src/common_main.cc; sourceTree = ""; }; + 529227201C85FB6A00C89379 /* main.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = main.h; path = src/main.h; sourceTree = ""; }; + 529227221C85FB7600C89379 /* ios_main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_main.mm; path = src/ios/ios_main.mm; sourceTree = ""; }; + 52B71EBA1C8600B600398745 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = testapp/Images.xcassets; sourceTree = ""; }; + 52FD1FF81C85FFA000BC68E3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = testapp/Info.plist; sourceTree = ""; }; + D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 529226CF1C85F68000C89379 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 529226D81C85F68000C89379 /* CoreGraphics.framework in Frameworks */, + 529226DA1C85F68000C89379 /* UIKit.framework in Frameworks */, + 529226D61C85F68000C89379 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 529226C91C85F68000C89379 = { + isa = PBXGroup; + children = ( + D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */, + 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */, + 52B71EBA1C8600B600398745 /* Images.xcassets */, + 52FD1FF81C85FFA000BC68E3 /* Info.plist */, + 5292271D1C85FB5500C89379 /* src */, + 529226D41C85F68000C89379 /* Frameworks */, + 529226D31C85F68000C89379 /* Products */, + ); + sourceTree = ""; + }; + 529226D31C85F68000C89379 /* Products */ = { + isa = PBXGroup; + children = ( + 529226D21C85F68000C89379 /* testapp.app */, + ); + name = Products; + sourceTree = ""; + }; + 529226D41C85F68000C89379 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 529226D51C85F68000C89379 /* Foundation.framework */, + 529226D71C85F68000C89379 /* CoreGraphics.framework */, + 529226D91C85F68000C89379 /* UIKit.framework */, + 529226EE1C85F68000C89379 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5292271D1C85FB5500C89379 /* src */ = { + isa = PBXGroup; + children = ( + 5292271F1C85FB6A00C89379 /* common_main.cc */, + 529227201C85FB6A00C89379 /* main.h */, + 5292271E1C85FB5B00C89379 /* ios */, + ); + name = src; + sourceTree = ""; + }; + 5292271E1C85FB5B00C89379 /* ios */ = { + isa = PBXGroup; + children = ( + 529227221C85FB7600C89379 /* ios_main.mm */, + ); + name = ios; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 529226D11C85F68000C89379 /* testapp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 529226F91C85F68000C89379 /* Build configuration list for PBXNativeTarget "testapp" */; + buildPhases = ( + 529226CE1C85F68000C89379 /* Sources */, + 529226CF1C85F68000C89379 /* Frameworks */, + 529226D01C85F68000C89379 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = testapp; + productName = testapp; + productReference = 529226D21C85F68000C89379 /* testapp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 529226CA1C85F68000C89379 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0640; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 529226D11C85F68000C89379 = { + CreatedOnToolsVersion = 6.4; + }; + }; + }; + buildConfigurationList = 529226CD1C85F68000C89379 /* Build configuration list for PBXProject "testapp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 529226C91C85F68000C89379; + productRefGroup = 529226D31C85F68000C89379 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 529226D11C85F68000C89379 /* testapp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 529226D01C85F68000C89379 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D66B16871CE46E8900E5638A /* LaunchScreen.storyboard in Resources */, + 52B71EBB1C8600B600398745 /* Images.xcassets in Resources */, + 520BC0391C869159008CFBC3 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 529226CE1C85F68000C89379 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 529227241C85FB7600C89379 /* ios_main.mm in Sources */, + 529227211C85FB6A00C89379 /* common_main.cc in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 529226F71C85F68000C89379 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 529226F81C85F68000C89379 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 529226FA1C85F68000C89379 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "\"$(SRCROOT)/src\"", + ); + INFOPLIST_FILE = testapp/Info.plist; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 529226FB1C85F68000C89379 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "\"$(SRCROOT)/src\"", + ); + INFOPLIST_FILE = testapp/Info.plist; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 529226CD1C85F68000C89379 /* Build configuration list for PBXProject "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 529226F71C85F68000C89379 /* Debug */, + 529226F81C85F68000C89379 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 529226F91C85F68000C89379 /* Build configuration list for PBXNativeTarget "testapp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 529226FA1C85F68000C89379 /* Debug */, + 529226FB1C85F68000C89379 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 529226CA1C85F68000C89379 /* Project object */; +} diff --git a/functions/testapp/testapp/Images.xcassets/AppIcon.appiconset/Contents.json b/functions/testapp/testapp/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..b7f3352e --- /dev/null +++ b/functions/testapp/testapp/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/functions/testapp/testapp/Images.xcassets/LaunchImage.launchimage/Contents.json b/functions/testapp/testapp/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..6f870a46 --- /dev/null +++ b/functions/testapp/testapp/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/functions/testapp/testapp/Info.plist b/functions/testapp/testapp/Info.plist new file mode 100644 index 00000000..11e393db --- /dev/null +++ b/functions/testapp/testapp/Info.plist @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.google.firebase.cpp.database.testapp + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + google + CFBundleURLSchemes + + com.googleusercontent.apps.255980362477-3a1nf8c4nl0c7hlnlnmc98hbtg2mnbue + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + + From f3f55d63827d85c2ec77b0c7ffcf27f157960dc8 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 11:30:58 -0400 Subject: [PATCH 02/10] replace Database -> Functions --- functions/testapp/Podfile | 2 +- functions/testapp/readme.md | 35 +++++----------------------- functions/testapp/src/common_main.cc | 2 +- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/functions/testapp/Podfile b/functions/testapp/Podfile index c3c902b9..0ad6fa00 100644 --- a/functions/testapp/Podfile +++ b/functions/testapp/Podfile @@ -1,6 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' -# Firebase Realtime Database test application. +# Firebase Cloud Functions test application. target 'testapp' do pod 'Firebase/Core' pod 'Firebase/Database' diff --git a/functions/testapp/readme.md b/functions/testapp/readme.md index 27100e87..178c58ec 100644 --- a/functions/testapp/readme.md +++ b/functions/testapp/readme.md @@ -1,41 +1,22 @@ -Firebase Realtime Database Quickstart +Firebase Cloud Functions Quickstart ======================== -The Firebase Realtime Database Test Application (testapp) demonstrates Firebase -Realtime Database operations with the Firebase Realtime Database C++ SDK. The +The Firebase Cloud Functions Test Application (testapp) demonstrates Firebase +Cloud Functions operations with the Firebase Cloud Functions C++ SDK. The application has no user interface and simply logs actions it's performing to the console. The testapp performs the following: - Creates a firebase::App in a platform-specific way. The App holds platform-specific context that's used by other Firebase APIs, and is a - central point for communication between the Firebase Realtime Database C++ + central point for communication between the Firebase Cloud Functions C++ and Firebase Auth C++ libraries. - - Gets a pointer to firebase::Auth, and signs in anonymously. This allows the - testapp to access a Firebase Database instance with authentication rules - enabled, which is the default setting in Firebase Console. - - Gets a DatabaseReference to the root node's "test_app_data" child, uses - DatabaseReference::PushChild() to create a child with a unique key - underneath it to work in, and gets a reference to that child, which the - testapp will use for the remainder of its actions. - - Sets some simple values (numbers, bools, strings, timestamp) and reads them - back to ensure the database can be read from and written to. - - Runs a transaction, using DatabaseReference::RunTransaction(), and validates - that its results were applied properly. - - Runs DatabaseReference::UpdateChildren to update multiple children at once. - - Uses Query to narrow down the view from a DatabaseReference. - - Sets up a ValueListener to watch for data value changes at a given database - location. - - Sets up a ChildListener to watch for changes in the list of children at - a database location. - - Sets up OnDisconnect actions to make changes to the database on disconnect, - then disconnects from the database to confirm the actions are performed. - Shuts down the Firebase Database, Firebase Auth, and Firebase App systems. Introduction ------------ -- [Read more about Firebase Realtime Database](https://firebase.google.com/docs/database/) +- [Read more about Firebase Cloud Functions](https://firebase.google.com/docs/functions/callable) Building and Running the testapp -------------------------------- @@ -118,10 +99,6 @@ Building and Running the testapp - Add the `google-services.json` file that you downloaded from Firebase console to the root directory of testapp. This file identifies your Android app to the Firebase backend. - - In the Firebase console for your app, select "Auth", then enable - "Anonymous". This will allow the testapp to use anonymous sign-in to - authenticate with Firebase Database, which requires a signed-in user by - default (an anonymous user will suffice). - For further details please refer to the [general instructions for setting up an Android app with Firebase](https://firebase.google.com/docs/android/setup). - Download the Firebase C++ SDK linked from @@ -193,7 +170,7 @@ Building and Running the testapp Known issues ------------ - - Due to the way Firebase Realtime Database interacts with JSON, it is + - Due to the way Firebase Cloud Functions interacts with JSON, it is possible that if you set a location to an integral value using a firebase::Variant of type Int64, you may get back a firebase::Variant of type Double when later retrieving the data from the database. Be sure to diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index 7d585c71..a718398a 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -372,7 +372,7 @@ extern "C" int common_main(int argc, const char* argv[]) { } #if defined(__ANDROID__) || TARGET_OS_IPHONE - // Actually shut down the realtime database, and restart it, to make sure + // Actually shut down the Cloud Functions, and restart it, to make sure // that persistence persists across database object instances. { // Write a value that we can test for. From 9de2aaad855ecc5a702b9ec83975ee772a7bd89e Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 11:30:58 -0400 Subject: [PATCH 03/10] replace Database -> Functions --- functions/testapp/AndroidManifest.xml | 2 +- functions/testapp/CMakeLists.txt | 2 +- functions/testapp/Podfile | 4 +- functions/testapp/build.gradle | 6 +-- functions/testapp/jni/Android.mk | 6 +-- functions/testapp/readme.md | 47 ++++++------------------ functions/testapp/res/values/strings.xml | 2 +- functions/testapp/src/common_main.cc | 2 +- functions/testapp/testapp/Info.plist | 2 +- 9 files changed, 25 insertions(+), 48 deletions(-) diff --git a/functions/testapp/AndroidManifest.xml b/functions/testapp/AndroidManifest.xml index db49c150..bb076723 100644 --- a/functions/testapp/AndroidManifest.xml +++ b/functions/testapp/AndroidManifest.xml @@ -1,6 +1,6 @@ diff --git a/functions/testapp/CMakeLists.txt b/functions/testapp/CMakeLists.txt index 3a40cb84..57c41072 100644 --- a/functions/testapp/CMakeLists.txt +++ b/functions/testapp/CMakeLists.txt @@ -70,7 +70,7 @@ endif() # Link Firebase libraries. # NOTE: firebase_app needs to be after all other Firebase libraries. link_directories(${FIREBASE_SDK_LIBDIR}) -set(FIREBASE_LIBS firebase_database firebase_auth firebase_app) +set(FIREBASE_LIBS firebase_functions firebase_auth firebase_app) # Add the Firebase include directory. set(FIREBASE_SDK_INCLUDEDIR ${FIREBASE_CPP_SDK_DIR}/include) diff --git a/functions/testapp/Podfile b/functions/testapp/Podfile index c3c902b9..f882b11a 100644 --- a/functions/testapp/Podfile +++ b/functions/testapp/Podfile @@ -1,8 +1,8 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' -# Firebase Realtime Database test application. +# Firebase Cloud Functions test application. target 'testapp' do pod 'Firebase/Core' - pod 'Firebase/Database' + pod 'Firebase/functions' pod 'Firebase/Auth' end diff --git a/functions/testapp/build.gradle b/functions/testapp/build.gradle index fcb9a0aa..31e82047 100644 --- a/functions/testapp/build.gradle +++ b/functions/testapp/build.gradle @@ -73,7 +73,7 @@ android { } defaultConfig { - applicationId 'com.google.firebase.cpp.database.testapp' + applicationId 'com.google.firebase.cpp.functions.testapp' minSdkVersion 14 targetSdkVersion 26 versionCode 1 @@ -85,7 +85,7 @@ android { proguardFile getDefaultProguardFile('proguard-android.txt') proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/app.pro") proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/auth.pro") - proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/database.pro") + proguardFile file(project.ext.firebase_cpp_sdk_dir + "/libs/android/functions.pro") proguardFile file('proguard.pro') } } @@ -94,7 +94,7 @@ android { dependencies { compile 'com.google.firebase:firebase-core:16.0.0' compile 'com.google.firebase:firebase-auth:16.0.1' - compile 'com.google.firebase:firebase-database:16.0.1' + compile 'com.google.firebase:firebase-functions:16.0.1' } apply plugin: 'com.google.gms.google-services' diff --git a/functions/testapp/jni/Android.mk b/functions/testapp/jni/Android.mk index cb38dc98..d7c13ceb 100644 --- a/functions/testapp/jni/Android.mk +++ b/functions/testapp/jni/Android.mk @@ -36,8 +36,8 @@ LOCAL_EXPORT_C_INCLUDES:=$(FIREBASE_CPP_SDK_DIR)/include include $(PREBUILT_STATIC_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE:=firebase_database -LOCAL_SRC_FILES:=$(FIREBASE_LIBRARY_PATH)/libfirebase_database.a +LOCAL_MODULE:=firebase_functions +LOCAL_SRC_FILES:=$(FIREBASE_LIBRARY_PATH)/libfirebase_functions.a LOCAL_EXPORT_C_INCLUDES:=$(FIREBASE_CPP_SDK_DIR)/include include $(PREBUILT_STATIC_LIBRARY) @@ -47,7 +47,7 @@ LOCAL_SRC_FILES:=\ $(LOCAL_PATH)/src/common_main.cc \ $(LOCAL_PATH)/src/android/android_main.cc LOCAL_STATIC_LIBRARIES:=\ - firebase_database \ + firebase_functions \ firebase_auth \ firebase_app LOCAL_WHOLE_STATIC_LIBRARIES:=\ diff --git a/functions/testapp/readme.md b/functions/testapp/readme.md index 27100e87..221564e7 100644 --- a/functions/testapp/readme.md +++ b/functions/testapp/readme.md @@ -1,41 +1,22 @@ -Firebase Realtime Database Quickstart +Firebase Cloud Functions Quickstart ======================== -The Firebase Realtime Database Test Application (testapp) demonstrates Firebase -Realtime Database operations with the Firebase Realtime Database C++ SDK. The +The Firebase Cloud Functions Test Application (testapp) demonstrates Firebase +Cloud Functions operations with the Firebase Cloud Functions C++ SDK. The application has no user interface and simply logs actions it's performing to the console. The testapp performs the following: - Creates a firebase::App in a platform-specific way. The App holds platform-specific context that's used by other Firebase APIs, and is a - central point for communication between the Firebase Realtime Database C++ + central point for communication between the Firebase Cloud Functions C++ and Firebase Auth C++ libraries. - - Gets a pointer to firebase::Auth, and signs in anonymously. This allows the - testapp to access a Firebase Database instance with authentication rules - enabled, which is the default setting in Firebase Console. - - Gets a DatabaseReference to the root node's "test_app_data" child, uses - DatabaseReference::PushChild() to create a child with a unique key - underneath it to work in, and gets a reference to that child, which the - testapp will use for the remainder of its actions. - - Sets some simple values (numbers, bools, strings, timestamp) and reads them - back to ensure the database can be read from and written to. - - Runs a transaction, using DatabaseReference::RunTransaction(), and validates - that its results were applied properly. - - Runs DatabaseReference::UpdateChildren to update multiple children at once. - - Uses Query to narrow down the view from a DatabaseReference. - - Sets up a ValueListener to watch for data value changes at a given database - location. - - Sets up a ChildListener to watch for changes in the list of children at - a database location. - - Sets up OnDisconnect actions to make changes to the database on disconnect, - then disconnects from the database to confirm the actions are performed. - - Shuts down the Firebase Database, Firebase Auth, and Firebase App systems. + - Shuts down the Firebase functions, Firebase Auth, and Firebase App systems. Introduction ------------ -- [Read more about Firebase Realtime Database](https://firebase.google.com/docs/database/) +- [Read more about Firebase Cloud Functions](https://firebase.google.com/docs/functions/callable) Building and Running the testapp -------------------------------- @@ -60,14 +41,14 @@ Building and Running the testapp - Register your iOS app with Firebase. - Create a new app on the [Firebase console](https://firebase.google.com/console/), and attach your iOS app to it. - - You can use "com.google.firebase.cpp.database.testapp" as the iOS Bundle + - You can use "com.google.firebase.cpp.functions.testapp" as the iOS Bundle ID while you're testing. You can omit App Store ID while testing. - Add the GoogleService-Info.plist that you downloaded from Firebase console to the testapp root directory. This file identifies your iOS app to the Firebase backend. - In the Firebase console for your app, select "Auth", then enable "Anonymous". This will allow the testapp to use anonymous sign-in to - authenticate with Firebase Database, which requires a signed-in user by + authenticate with Firebase functions, which requires a signed-in user by default (an anonymous user will suffice). - Download the Firebase C++ SDK linked from [https://firebase.google.com/docs/cpp/setup]() and unzip it to a @@ -75,7 +56,7 @@ Building and Running the testapp - Add the following frameworks from the Firebase C++ SDK to the project: - frameworks/ios/universal/firebase.framework - frameworks/ios/universal/firebase_auth.framework - - frameworks/ios/universal/firebase_database.framework + - frameworks/ios/universal/firebase_functions.framework - You will need to either, 1. Check "Copy items if needed" when adding the frameworks, or 2. Add the framework path in "Framework Search Paths" @@ -98,7 +79,7 @@ Building and Running the testapp - Create a new app on the [Firebase console](https://firebase.google.com/console/), and attach your Android app to it. - - You can use "com.google.firebase.cpp.database.testapp" as the Package + - You can use "com.google.firebase.cpp.functions.testapp" as the Package Name while you're testing. - To [generate a SHA1](https://developers.google.com/android/guides/client-auth) @@ -118,10 +99,6 @@ Building and Running the testapp - Add the `google-services.json` file that you downloaded from Firebase console to the root directory of testapp. This file identifies your Android app to the Firebase backend. - - In the Firebase console for your app, select "Auth", then enable - "Anonymous". This will allow the testapp to use anonymous sign-in to - authenticate with Firebase Database, which requires a signed-in user by - default (an anonymous user will suffice). - For further details please refer to the [general instructions for setting up an Android app with Firebase](https://firebase.google.com/docs/android/setup). - Download the Firebase C++ SDK linked from @@ -193,10 +170,10 @@ Building and Running the testapp Known issues ------------ - - Due to the way Firebase Realtime Database interacts with JSON, it is + - Due to the way Firebase Cloud Functions interacts with JSON, it is possible that if you set a location to an integral value using a firebase::Variant of type Int64, you may get back a firebase::Variant of - type Double when later retrieving the data from the database. Be sure to + type Double when later retrieving the data from the functions. Be sure to check the types of Variants or use methods such as Variant::AsInt64() to coerce their types before accessing the data within them. diff --git a/functions/testapp/res/values/strings.xml b/functions/testapp/res/values/strings.xml index eef9e385..4806f98b 100644 --- a/functions/testapp/res/values/strings.xml +++ b/functions/testapp/res/values/strings.xml @@ -1,4 +1,4 @@ - Firebase Database Test + Firebase functions Test diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index 7d585c71..a718398a 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -372,7 +372,7 @@ extern "C" int common_main(int argc, const char* argv[]) { } #if defined(__ANDROID__) || TARGET_OS_IPHONE - // Actually shut down the realtime database, and restart it, to make sure + // Actually shut down the Cloud Functions, and restart it, to make sure // that persistence persists across database object instances. { // Write a value that we can test for. diff --git a/functions/testapp/testapp/Info.plist b/functions/testapp/testapp/Info.plist index 11e393db..988d7587 100644 --- a/functions/testapp/testapp/Info.plist +++ b/functions/testapp/testapp/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.google.firebase.cpp.database.testapp + com.google.firebase.cpp.functions.testapp CFBundleInfoDictionaryVersion 6.0 CFBundleName From d6e1f97e7a3ac2921194c590546ddacd618813f2 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 13:42:51 -0400 Subject: [PATCH 04/10] First build + ignores --- functions/testapp/.gitignore | 6 +++++ functions/testapp/build.gradle | 44 +++++++++++++--------------------- 2 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 functions/testapp/.gitignore diff --git a/functions/testapp/.gitignore b/functions/testapp/.gitignore new file mode 100644 index 00000000..0679330e --- /dev/null +++ b/functions/testapp/.gitignore @@ -0,0 +1,6 @@ +build/ +.* +!/.gitignore +google-services.json +local.properties +*.iml \ No newline at end of file diff --git a/functions/testapp/build.gradle b/functions/testapp/build.gradle index 31e82047..86b4086c 100644 --- a/functions/testapp/build.gradle +++ b/functions/testapp/build.gradle @@ -4,9 +4,10 @@ buildscript { mavenLocal() maven { url 'https://maven.google.com' } jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' + classpath 'com.android.tools.build:gradle:3.1.3' classpath 'com.google.gms:google-services:4.0.1' } } @@ -78,6 +79,17 @@ android { targetSdkVersion 26 versionCode 1 versionName '1.0' + + externalNativeBuild { + ndkBuild { + arguments "FIREBASE_CPP_SDK_DIR=${project.ext.firebase_cpp_sdk_dir}" + } + } + } + externalNativeBuild { + ndkBuild { + path file("jni/Android.mk") + } } buildTypes { release { @@ -92,31 +104,9 @@ android { } dependencies { - compile 'com.google.firebase:firebase-core:16.0.0' - compile 'com.google.firebase:firebase-auth:16.0.1' - compile 'com.google.firebase:firebase-functions:16.0.1' -} - -apply plugin: 'com.google.gms.google-services' - -task ndkBuildCompile(type:Exec) { - description 'Use ndk-build to compile the C++ application.' - commandLine("${project.ext.ndk_dir}${File.separator}ndk-build", - "FIREBASE_CPP_SDK_DIR=${project.ext.firebase_cpp_sdk_dir}", - sprintf("APP_PLATFORM=android-%d", - android.defaultConfig.minSdkVersion.mApiLevel)) -} - -task ndkBuildClean(type:Exec) { - description 'Use ndk-build to clean the C++ application.' - commandLine("${project.ext.ndk_dir}${File.separator}ndk-build", - "FIREBASE_CPP_SDK_DIR=${project.ext.firebase_cpp_sdk_dir}", - "clean") + implementation 'com.google.firebase:firebase-core:16.0.0' + implementation 'com.google.firebase:firebase-auth:16.0.1' + implementation 'com.google.firebase:firebase-functions:16.1.0' } -// Once the Android Gradle plug-in has generated tasks, add dependencies for -// the ndk-build targets. -project.afterEvaluate { - preBuild.dependsOn(ndkBuildCompile) - clean.dependsOn(ndkBuildClean) -} +apply plugin: 'com.google.gms.google-services' \ No newline at end of file From b698deff8f68da4e8ca2556e5b656d7e91ff1364 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 13:47:58 -0400 Subject: [PATCH 05/10] First Run! --- functions/testapp/src/common_main.cc | 997 +-------------------------- 1 file changed, 1 insertion(+), 996 deletions(-) diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index a718398a..a30516ca 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -16,117 +16,13 @@ #include #include "firebase/app.h" #include "firebase/auth.h" -#include "firebase/database.h" +#include "firebase/functions.h" #include "firebase/future.h" #include "firebase/util.h" // Thin OS abstraction layer. #include "main.h" // NOLINT -// An example of a ValueListener object. This specific version will -// simply log every value it sees, and store them in a list so we can -// confirm that all values were received. -class SampleValueListener : public firebase::database::ValueListener { - public: - void OnValueChanged( - const firebase::database::DataSnapshot& snapshot) override { - LogMessage(" ValueListener.OnValueChanged(%s)", - snapshot.value().AsString().string_value()); - last_seen_value_ = snapshot.value(); - seen_values_.push_back(snapshot.value()); - } - void OnCancelled(const firebase::database::Error& error_code, - const char* error_message) override { - LogMessage("ERROR: SampleValueListener canceled: %d: %s", error_code, - error_message); - } - const firebase::Variant& last_seen_value() { return last_seen_value_; } - bool seen_value(const firebase::Variant& value) { - return std::find(seen_values_.begin(), seen_values_.end(), value) != - seen_values_.end(); - } - size_t num_seen_values() { return seen_values_.size(); } - - private: - firebase::Variant last_seen_value_; - std::vector seen_values_; -}; - -// An example ChildListener class. TODO(jsimantov) implement. -class SampleChildListener : public firebase::database::ChildListener { - public: - void OnChildAdded(const firebase::database::DataSnapshot& snapshot, - const char* previous_sibling) override { - LogMessage(" ChildListener.OnChildAdded(%s)", snapshot.key()); - events_.push_back(std::string("added ") + snapshot.key()); - } - void OnChildChanged(const firebase::database::DataSnapshot& snapshot, - const char* previous_sibling) override { - LogMessage(" ChildListener.OnChildChanged(%s)", snapshot.key()); - events_.push_back(std::string("changed ") + snapshot.key()); - } - void OnChildMoved(const firebase::database::DataSnapshot& snapshot, - const char* previous_sibling) override { - LogMessage(" ChildListener.OnChildMoved(%s)", snapshot.key()); - events_.push_back(std::string("moved ") + snapshot.key()); - } - void OnChildRemoved( - const firebase::database::DataSnapshot& snapshot) override { - LogMessage(" ChildListener.OnChildRemoved(%s)", snapshot.key()); - events_.push_back(std::string("removed ") + snapshot.key()); - } - void OnCancelled(const firebase::database::Error& error_code, - const char* error_message) override { - LogMessage("ERROR: SampleChildListener canceled: %d: %s", error_code, - error_message); - } - - // Get the total number of Child events this listener saw. - size_t total_events() { return events_.size(); } - - // Get the number of times this event was seen. - int num_events(const std::string& event) { - int count = 0; - for (int i = 0; i < events_.size(); i++) { - if (events_[i] == event) { - count++; - } - } - return count; - } - - public: - // Vector of strings defining the events we saw, in order. - std::vector events_; -}; - -// A ValueListener that expects a specific value to be set. -class ExpectValueListener : public firebase::database::ValueListener { - public: - explicit ExpectValueListener(firebase::Variant wait_value) - : wait_value_(wait_value.AsString()), got_value_(false) {} - void OnValueChanged( - const firebase::database::DataSnapshot& snapshot) override { - if (snapshot.value().AsString() == wait_value_) { - got_value_ = true; - } else { - LogMessage( - "FAILURE: ExpectValueListener did not receive the expected result."); - } - } - void OnCancelled(const firebase::database::Error& error_code, - const char* error_message) override { - LogMessage("ERROR: ExpectValueListener canceled: %d: %s", error_code, - error_message); - } - - bool got_value() { return got_value_; } - - private: - firebase::Variant wait_value_; - bool got_value_; -}; - // Wait for a Future to be completed. If the Future returns an error, it will // be logged. void WaitForCompletion(const firebase::FutureBase& future, const char* name) { @@ -152,897 +48,6 @@ extern "C" int common_main(int argc, const char* argv[]) { LogMessage("Initialized Firebase App."); - LogMessage("Initialize Firebase Auth and Firebase Database."); - - // Use ModuleInitializer to initialize both Auth and Database, ensuring no - // dependencies are missing. - ::firebase::database::Database* database = nullptr; - ::firebase::auth::Auth* auth = nullptr; - void* initialize_targets[] = {&auth, &database}; - - const firebase::ModuleInitializer::InitializerFn initializers[] = { - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Auth."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = - ::firebase::auth::Auth::GetAuth(app, &result); - return result; - }, - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Database."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::database::Database**>(targets[1]) = - ::firebase::database::Database::GetInstance(app, &result); - return result; - }}; - - ::firebase::ModuleInitializer initializer; - initializer.Initialize(app, initialize_targets, initializers, - sizeof(initializers) / sizeof(initializers[0])); - - WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); - - if (initializer.InitializeLastResult().error() != 0) { - LogMessage("Failed to initialize Firebase libraries: %s", - initializer.InitializeLastResult().error_message()); - ProcessEvents(2000); - return 1; - } - LogMessage("Successfully initialized Firebase Auth and Firebase Database."); - - database->set_persistence_enabled(true); - - // Sign in using Auth before accessing the database. - // The default Database permissions allow anonymous users access. This will - // work as long as your project's Authentication permissions allow anonymous - // signin. - { - firebase::Future sign_in_future = - auth->SignInAnonymously(); - WaitForCompletion(sign_in_future, "SignInAnonymously"); - if (sign_in_future.error() == firebase::auth::kAuthErrorNone) { - LogMessage("Auth: Signed in anonymously."); - } else { - LogMessage("ERROR: Could not sign in anonymously. Error %d: %s", - sign_in_future.error(), sign_in_future.error_message()); - LogMessage( - " Ensure your application has the Anonymous sign-in provider " - "enabled in Firebase Console."); - LogMessage( - " Attempting to connect to the database anyway. This may fail " - "depending on the security settings."); - } - } - - std::string saved_url; // persists across connections - - // Create a unique child in the database that we can run our tests in. - firebase::database::DatabaseReference ref; - ref = database->GetReference("test_app_data").PushChild(); - - saved_url = ref.url(); - LogMessage("URL: %s", saved_url.c_str()); - - // Set and Get some simple fields. This will set a string, integer, double, - // bool, and current timestamp, and then read them back from the database to - // confirm that they were set. Then it will remove the string value. - { - const char* kSimpleString = "Some simple string"; - const int kSimpleInt = 2; - const int kSimplePriority = 100; - const double kSimpleDouble = 3.4; - const bool kSimpleBool = true; - - { - LogMessage("TEST: Set simple values."); - firebase::Future f1 = - ref.Child("Simple").Child("String").SetValue(kSimpleString); - firebase::Future f2 = - ref.Child("Simple").Child("Int").SetValue(kSimpleInt); - firebase::Future f3 = - ref.Child("Simple").Child("Double").SetValue(kSimpleDouble); - firebase::Future f4 = - ref.Child("Simple").Child("Bool").SetValue(kSimpleBool); - firebase::Future f5 = - ref.Child("Simple") - .Child("Timestamp") - .SetValue(firebase::database::ServerTimestamp()); - firebase::Future f6 = - ref.Child("Simple") - .Child("IntAndPriority") - .SetValueAndPriority(kSimpleInt, kSimplePriority); - WaitForCompletion(f1, "SetSimpleString"); - WaitForCompletion(f2, "SetSimpleInt"); - WaitForCompletion(f3, "SetSimpleDouble"); - WaitForCompletion(f4, "SetSimpleBool"); - WaitForCompletion(f5, "SetSimpleTimestamp"); - WaitForCompletion(f6, "SetSimpleIntAndPriority"); - if (f1.error() != firebase::database::kErrorNone || - f2.error() != firebase::database::kErrorNone || - f3.error() != firebase::database::kErrorNone || - f4.error() != firebase::database::kErrorNone || - f5.error() != firebase::database::kErrorNone || - f6.error() != firebase::database::kErrorNone) { - LogMessage("ERROR: Set simple values failed."); - LogMessage(" String: Error %d: %s", f1.error(), f1.error_message()); - LogMessage(" Int: Error %d: %s", f2.error(), f2.error_message()); - LogMessage(" Double: Error %d: %s", f3.error(), f3.error_message()); - LogMessage(" Bool: Error %d: %s", f4.error(), f4.error_message()); - LogMessage(" Timestamp: Error %d: %s", f5.error(), f5.error_message()); - LogMessage(" Int and Priority: Error %d: %s", f6.error(), - f6.error_message()); - } else { - LogMessage("SUCCESS: Set simple values."); - } - } - // Get the values that we just set, and confirm that they match what we - // set them to. - { - LogMessage("TEST: Get simple values."); - firebase::Future f1 = - ref.Child("Simple").Child("String").GetValue(); - firebase::Future f2 = - ref.Child("Simple").Child("Int").GetValue(); - firebase::Future f3 = - ref.Child("Simple").Child("Double").GetValue(); - firebase::Future f4 = - ref.Child("Simple").Child("Bool").GetValue(); - firebase::Future f5 = - ref.Child("Simple").Child("Timestamp").GetValue(); - firebase::Future f6 = - ref.Child("Simple").Child("IntAndPriority").GetValue(); - WaitForCompletion(f1, "GetSimpleString"); - WaitForCompletion(f2, "GetSimpleInt"); - WaitForCompletion(f3, "GetSimpleDouble"); - WaitForCompletion(f4, "GetSimpleBool"); - WaitForCompletion(f5, "GetSimpleTimestamp"); - WaitForCompletion(f6, "GetSimpleIntAndPriority"); - - if (f1.error() == firebase::database::kErrorNone && - f2.error() == firebase::database::kErrorNone && - f3.error() == firebase::database::kErrorNone && - f4.error() == firebase::database::kErrorNone && - f5.error() == firebase::database::kErrorNone && - f6.error() == firebase::database::kErrorNone) { - // Get the current time to compare to the Timestamp. - int64_t current_time_milliseconds = - static_cast(time(nullptr)) * 1000L; - int64_t time_difference = f5.result()->value().AsInt64().int64_value() - - current_time_milliseconds; - // As long as our timestamp is within a day, it's correct enough for our - // purposes. - const int64_t kAllowedTimeDifferenceMilliseconds = - 1000L * 60L * 60L * 24L; - - if (f1.result()->value().AsString() != kSimpleString || - f2.result()->value().AsInt64() != kSimpleInt || - f3.result()->value().AsDouble() != kSimpleDouble || - f4.result()->value().AsBool() != kSimpleBool || - f6.result()->value().AsInt64() != kSimpleInt || - f6.result()->priority().AsInt64() != kSimplePriority || - time_difference > kAllowedTimeDifferenceMilliseconds || - time_difference < -kAllowedTimeDifferenceMilliseconds) { - LogMessage("ERROR: Get simple values failed, values did not match."); - LogMessage(" String: Got \"%s\", expected \"%s\"", - f1.result()->value().string_value(), kSimpleString); - LogMessage(" Int: Got %lld, expected %d", - f2.result()->value().AsInt64().int64_value(), kSimpleInt); - LogMessage(" Double: Got %lf, expected %lf", - f3.result()->value().AsDouble().double_value(), - kSimpleDouble); - LogMessage( - " Bool: Got %s, expected %s", - f4.result()->value().AsBool().bool_value() ? "true" : "false", - kSimpleBool ? "true" : "false"); - LogMessage(" Timestamp: Got %lld, expected something near %lld", - f5.result()->value().AsInt64().int64_value(), - current_time_milliseconds); - LogMessage( - " IntAndPriority: Got {.value:%lld,.priority:%lld}, expected " - "{.value:%d,.priority:%d}", - f6.result()->value().AsInt64().int64_value(), - f6.result()->priority().AsInt64().int64_value(), kSimpleInt, - kSimplePriority); - - } else { - LogMessage("SUCCESS: Get simple values."); - } - } else { - LogMessage("ERROR: Get simple values failed."); - } - - // Try removing one value. - { - LogMessage("TEST: Removing a value."); - WaitForCompletion(ref.Child("Simple").Child("String").RemoveValue(), - "RemoveSimpleString"); - firebase::Future future = - ref.Child("Simple").Child("String").GetValue(); - WaitForCompletion(future, "GetRemovedSimpleString"); - if (future.error() == firebase::database::kErrorNone && - future.result()->value().is_null()) { - LogMessage("SUCCESS: Value was removed."); - } else { - LogMessage("ERROR: Value was not removed."); - } - } - } - } - -#if defined(__ANDROID__) || TARGET_OS_IPHONE - // Actually shut down the Cloud Functions, and restart it, to make sure - // that persistence persists across database object instances. - { - // Write a value that we can test for. - const char* kPersistenceString = "Persistence Test!"; - WaitForCompletion(ref.Child("PersistenceTest").SetValue(kPersistenceString), - "SetPersistenceTestValue"); - - LogMessage("Destroying database object."); - delete database; - LogMessage("Recreating database object."); - database = ::firebase::database::Database::GetInstance(app); - - // Offline mode. If persistence works, we should still be able to fetch - // our value even though we're offline. - - database->GoOffline(); - ref = database->GetReferenceFromUrl(saved_url.c_str()); - - { - LogMessage( - "TEST: Fetching the value while offline via AddValueListener."); - ExpectValueListener* listener = - new ExpectValueListener(kPersistenceString); - ref.Child("PersistenceTest").AddValueListener(listener); - - while (!listener->got_value()) { - ProcessEvents(100); - } - delete listener; - listener = nullptr; - } - - { - LogMessage("TEST: Fetching the value while offline via GetValue."); - firebase::Future value_future = - ref.Child("PersistenceTest").GetValue(); - - WaitForCompletion(value_future, "GetValue"); - - const firebase::database::DataSnapshot& result = *value_future.result(); - - if (value_future.error() == firebase::database::kErrorNone) { - if (result.value().AsString() == kPersistenceString) { - LogMessage("SUCCESS: GetValue returned the correct value."); - } else { - LogMessage("FAILURE: GetValue returned an incorrect value."); - } - } else { - LogMessage("FAILURE: GetValue Future returned an error."); - } - } - - LogMessage("Going back online."); - database->GoOnline(); - } -#endif // defined(__ANDROID__) || TARGET_OS_IPHONE - - // Test running a transaction. This will call RunTransaction and set - // some values, including incrementing the player's score. - { - firebase::Future transaction_future; - static const int kInitialScore = 500; - static const int kAddedScore = 100; - LogMessage("TEST: Run transaction."); - // Set an initial score of 500 points. - WaitForCompletion(ref.Child("TransactionResult") - .Child("player_score") - .SetValue(kInitialScore), - "SetInitialScoreValue"); - // The transaction will set the player's item and class, and increment - // their score by 100 points. - int score_delta = 100; - transaction_future = - ref.Child("TransactionResult") - .RunTransaction( - [](firebase::database::MutableData* data, - void* score_delta_void) { - LogMessage(" Transaction function executing."); - data->Child("player_item").set_value("Fire sword"); - data->Child("player_class").set_value("Warrior"); - // Increment the current score by 100. - int64_t score = data->Child("player_score") - .value() - .AsInt64() - .int64_value(); - data->Child("player_score") - .set_value(score + - *reinterpret_cast(score_delta_void)); - return firebase::database::kTransactionResultSuccess; - }, - &score_delta); - WaitForCompletion(transaction_future, "RunTransaction"); - - // Check whether the transaction succeeded, was aborted, or failed with an - // error. - if (transaction_future.error() == firebase::database::kErrorNone) { - LogMessage("SUCCESS: Transaction committed."); - } else if (transaction_future.error() == - firebase::database::kErrorTransactionAbortedByUser) { - LogMessage("ERROR: Transaction was aborted."); - } else { - LogMessage("ERROR: Transaction returned error %d: %s", - transaction_future.error(), - transaction_future.error_message()); - } - - // If the transaction succeeded, let's read back the values that were - // written to confirm they match. - if (transaction_future.error() == firebase::database::kErrorNone) { - LogMessage("TEST: Test reading transaction results."); - - firebase::Future read_future = - ref.Child("TransactionResult").GetValue(); - WaitForCompletion(read_future, "ReadTransactionResults"); - if (read_future.error() != firebase::database::kErrorNone) { - LogMessage("ERROR: Error %d reading transaction results: %s", - read_future.error(), read_future.error_message()); - } else { - const firebase::database::DataSnapshot& result = *read_future.result(); - if (result.children_count() == 3 && result.HasChild("player_item") && - result.Child("player_item").value() == "Fire sword" && - result.HasChild("player_class") && - result.Child("player_class").value() == "Warrior" && - result.HasChild("player_score") && - result.Child("player_score").value().AsInt64() == - kInitialScore + kAddedScore) { - if (result.value() != transaction_future.result()->value()) { - LogMessage( - "ERROR: Transaction snapshot did not match newly read data."); - } else { - LogMessage("SUCCESS: Transaction test succeeded."); - } - } else { - LogMessage("ERROR: Transaction result was incorrect."); - } - } - } - } - - // Set up a map of values that we will put into the database, then modify. - std::map sample_values; - sample_values.insert(std::make_pair("Apple", 1)); - sample_values.insert(std::make_pair("Banana", 2)); - sample_values.insert(std::make_pair("Cranberry", 3)); - sample_values.insert(std::make_pair("Durian", 4)); - sample_values.insert(std::make_pair("Eggplant", 5)); - - // Run UpdateChildren, specifying some existing children (which will be - // modified), some children with a value of null (which will be removed), - // and some new children (which will be added). - { - LogMessage("TEST: UpdateChildren."); - - WaitForCompletion(ref.Child("UpdateChildren").SetValue(sample_values), - "UpdateSetValues"); - - // Set each key's value to what's given in this map. We use a map of - // Variant so that we can specify Variant::Null() to remove a key from the - // database. - std::map update_values; - update_values.insert(std::make_pair("Apple", 100)); - update_values.insert(std::make_pair("Durian", "is a fruit!")); - update_values.insert(std::make_pair("Eggplant", firebase::Variant::Null())); - update_values.insert(std::make_pair("Fig", 6)); - - WaitForCompletion(ref.Child("UpdateChildren").UpdateChildren(update_values), - "UpdateChildren"); - - // Get the values that were written to ensure they were updated properly. - firebase::Future updated_values = - ref.Child("UpdateChildren").GetValue(); - WaitForCompletion(updated_values, "UpdateChildrenResult"); - if (updated_values.error() == firebase::database::kErrorNone) { - const firebase::database::DataSnapshot& result = *updated_values.result(); - bool failed = false; - if (result.children_count() != 5) { - LogMessage( - "ERROR: UpdateChildren returned an unexpected number of " - "children: " - "%d", - result.children_count()); - failed = true; - } - if (!result.HasChild("Apple") || - result.Child("Apple").value().AsInt64() != 100) { - LogMessage("ERROR: Child key 'Apple' was not updated correctly."); - failed = true; - } - if (!result.HasChild("Banana") || - result.Child("Banana").value().AsInt64() != 2) { - LogMessage("ERROR: Child key 'Banana' was not updated correctly."); - failed = true; - } - if (!result.HasChild("Cranberry") || - result.Child("Cranberry").value().AsInt64() != 3) { - LogMessage("ERROR: Child key 'Cranberry' was not updated correctly."); - failed = true; - } - if (!result.HasChild("Durian") || - result.Child("Durian").value().AsString() != "is a fruit!") { - LogMessage("ERROR: Child key 'Durian' was not updated correctly."); - failed = true; - } - if (result.HasChild("Eggplant")) { - LogMessage("ERROR: Child key 'Eggplant' was not removed."); - failed = true; - } - if (!result.HasChild("Fig") || - result.Child("Fig").value().AsInt64() != 6) { - LogMessage("ERROR: Child key 'Fig' was not added correctly."); - failed = true; - } - if (!failed) { - LogMessage("SUCCESS: UpdateChildren succeeded."); - } else { - LogMessage( - "ERROR: UpdateChildren did not modify the children as expected."); - } - } else { - LogMessage("ERROR: Couldn't retrieve updated values."); - } - } - - // Test Query, which gives you different views into the same location in the - // database. - { - LogMessage("TEST: Query filtering."); - - firebase::Future set_future = - ref.Child("QueryFiltering").SetValue(sample_values); - WaitForCompletion(set_future, "QuerySetValues"); - // Create a query for keys in the lexicographical range "B" to "Dz". - auto b_to_d = ref.Child("QueryFiltering") - .OrderByKey() - .StartAt("B") - .EndAt("Dz") - .GetValue(); - // Create a query for values in the numeric range 1 to 3. - auto one_to_three = ref.Child("QueryFiltering") - .OrderByValue() - .StartAt(1) - .EndAt(3) - .GetValue(); - // Create a query ordered by value, but limited to only the highest two - // values. - auto four_and_five = - ref.Child("QueryFiltering").OrderByValue().LimitToLast(2).GetValue(); - // Create a query ordered by key, but limited to only the lowest two keys. - auto a_and_b = - ref.Child("QueryFiltering").OrderByKey().LimitToFirst(2).GetValue(); - // Create a query limited only to the key "Cranberry". - auto c_only = ref.Child("QueryFiltering") - .OrderByKey() - .EqualTo("Cranberry") - .GetValue(); - - WaitForCompletion(b_to_d, "QueryBthruD"); - WaitForCompletion(one_to_three, "Query1to3"); - WaitForCompletion(four_and_five, "Query4and5"); - WaitForCompletion(a_and_b, "QueryAandB"); - WaitForCompletion(c_only, "QueryC"); - - bool failed = false; - // Check that the queries each returned the expected results. - if (b_to_d.error() != firebase::database::kErrorNone || - b_to_d.result()->children_count() != 3 || - !b_to_d.result()->HasChild("Banana") || - !b_to_d.result()->HasChild("Cranberry") || - !b_to_d.result()->HasChild("Durian")) { - LogMessage("ERROR: Query B-to-D returned unexpected results."); - failed = true; - } - if (one_to_three.error() != firebase::database::kErrorNone || - one_to_three.result()->children_count() != 3 || - !one_to_three.result()->HasChild("Apple") || - !one_to_three.result()->HasChild("Banana") || - !one_to_three.result()->HasChild("Cranberry")) { - LogMessage("ERROR: Query 1-to-3 returned unexpected results."); - failed = true; - } - if (four_and_five.error() != firebase::database::kErrorNone || - four_and_five.result()->children_count() != 2 || - !four_and_five.result()->HasChild("Durian") || - !four_and_five.result()->HasChild("Eggplant")) { - LogMessage("ERROR: Query 4-and-5 returned unexpected results."); - failed = true; - } - if (a_and_b.error() != firebase::database::kErrorNone || - a_and_b.result()->children_count() != 2 || - !a_and_b.result()->HasChild("Apple") || - !a_and_b.result()->HasChild("Banana")) { - LogMessage("ERROR: Query A-and-B returned unexpected results."); - failed = true; - } - if (c_only.error() != firebase::database::kErrorNone || - c_only.result()->children_count() != 1 || - !c_only.result()->HasChild("Cranberry")) { - LogMessage("ERROR: Query C-only returned unexpected results."); - failed = true; - } - if (!failed) { - LogMessage("SUCCESS: Query filtering succeeded."); - } - } - - // Test a ValueListener, which sits on a Query and listens for changes in - // the value at that location. - { - LogMessage("TEST: ValueListener"); - SampleValueListener* listener = new SampleValueListener(); - WaitForCompletion(ref.Child("ValueListener").SetValue(0), "SetValueZero"); - // Attach the listener, then set 3 values, which will trigger the - // listener. - ref.Child("ValueListener").AddValueListener(listener); - - // The listener's OnChanged callback is triggered once when the listener is - // attached and again every time the data, including children, changes. - // Wait for here for a moment for the initial values to be received. - ProcessEvents(2000); - - WaitForCompletion(ref.Child("ValueListener").SetValue(1), "SetValueOne"); - WaitForCompletion(ref.Child("ValueListener").SetValue(2), "SetValueTwo"); - WaitForCompletion(ref.Child("ValueListener").SetValue(3), "SetValueThree"); - - LogMessage(" Waiting for ValueListener..."); - - // Wait a few seconds for the value listener to be triggered. - ProcessEvents(2000); - - // Unregister the listener, so it stops triggering. - ref.Child("ValueListener").RemoveValueListener(listener); - - // Ensure that the listener is not triggered once removed. - WaitForCompletion(ref.Child("ValueListener").SetValue(4), "SetValueFour"); - - // Wait a few more seconds to ensure the listener is not triggered. - ProcessEvents(2000); - - // Ensure that the listener was only triggered 4 times, with the values - // 0 (the initial value), 1, 2, and 3. - if (listener->num_seen_values() == 4 && listener->seen_value(0) && - listener->seen_value(1) && listener->seen_value(2) && - listener->seen_value(3)) { - LogMessage("SUCCESS: ValueListener got all values."); - } else { - LogMessage("ERROR: ValueListener did not get all values."); - } - - delete listener; - } - // Test a ChildListener, which sits on a Query and listens for changes in - // the child hierarchy at the location. - { - LogMessage("TEST: ChildListener"); - SampleChildListener* listener = new SampleChildListener(); - - // Set a child listener that only listens for entities of type "enemy". - auto entity_list = ref.Child("ChildListener").Child("entity_list"); - - entity_list.OrderByChild("entity_type") - .EqualTo("enemy") - .AddChildListener(listener); - - // The listener's OnChanged callback is triggered once when the listener is - // attached and again every time the data, including children, changes. - // Wait for here for a moment for the initial values to be received. - ProcessEvents(2000); - - std::map params; - params["entity_name"] = "cobra"; - params["entity_type"] = "enemy"; - WaitForCompletion(entity_list.Child("0").SetValueAndPriority(params, 0), - "SetEntity0"); - params["entity_name"] = "warrior"; - params["entity_type"] = "hero"; - WaitForCompletion(entity_list.Child("1").SetValueAndPriority(params, 10), - "SetEntity1"); - params["entity_name"] = "wizard"; - params["entity_type"] = "hero"; - WaitForCompletion(entity_list.Child("2").SetValueAndPriority(params, 20), - "SetEntity2"); - params["entity_name"] = "rat"; - params["entity_type"] = "enemy"; - WaitForCompletion(entity_list.Child("3").SetValueAndPriority(params, 30), - "SetEntity3"); - params["entity_name"] = "thief"; - params["entity_type"] = "enemy"; - WaitForCompletion(entity_list.Child("4").SetValueAndPriority(params, 40), - "SetEntity4"); - params["entity_name"] = "paladin"; - params["entity_type"] = "hero"; - WaitForCompletion(entity_list.Child("5").SetValueAndPriority(params, 50), - "SetEntity5"); - params["entity_name"] = "ghost"; - params["entity_type"] = "enemy"; - WaitForCompletion(entity_list.Child("6").SetValueAndPriority(params, 60), - "SetEntity6"); - params["entity_name"] = "dragon"; - params["entity_type"] = "enemy"; - WaitForCompletion(entity_list.Child("7").SetValueAndPriority(params, 70), - "SetEntity7"); - // Now the thief becomes a hero! - WaitForCompletion( - entity_list.Child("4").Child("entity_type").SetValue("hero"), - "SetEntity4Type"); - // Now the dragon becomes a super-dragon! - WaitForCompletion( - entity_list.Child("7").Child("entity_name").SetValue("super-dragon"), - "SetEntity7Name"); - // Now the super-dragon becomes an mega-dragon! - WaitForCompletion( - entity_list.Child("7").Child("entity_name").SetValue("mega-dragon"), - "SetEntity7NameAgain"); - // And now we change a hero entity, which the Query ignores. - WaitForCompletion( - entity_list.Child("2").Child("entity_name").SetValue("super-wizard"), - "SetEntity2Value"); - // Now poof, the mega-dragon is gone. - WaitForCompletion(entity_list.Child("7").RemoveValue(), "RemoveEntity7"); - - LogMessage(" Waiting for ChildListener..."); - - // Wait a few seconds for the child listener to be triggered. - ProcessEvents(2000); - - // Unregister the listener, so it stops triggering. - entity_list.OrderByChild("entity_type") - .EqualTo("enemy") - .RemoveChildListener(listener); - - // Wait a few seconds for the child listener to be triggered. - ProcessEvents(2000); - - // Make one more change, to ensure the listener has been removed. - WaitForCompletion(entity_list.Child("6").SetPriority(0), - "SetEntity6Priority"); - - // We are expecting to have the following events: - bool failed = false; - if (listener->num_events("added 0") != 1) { - LogMessage( - "ERROR: OnChildAdded(0) was called an incorrect number of times."); - failed = true; - } - if (listener->num_events("added 3") != 1) { - LogMessage( - "ERROR: OnChildAdded(3) was called an incorrect number of times."); - failed = true; - } - if (listener->num_events("added 4") != 1) { - LogMessage( - "ERROR: OnChildAdded(4) was called an incorrect number of times."); - failed = true; - } - if (listener->num_events("added 6") != 1) { - LogMessage( - "ERROR: OnChildAdded(6) was called an incorrect number of times."); - failed = true; - } - if (listener->num_events("added 7") != 1) { - LogMessage( - "ERROR: OnChildAdded(7) was called an incorrect number of times."); - failed = true; - } - if (listener->num_events("removed 4") != 1) { - LogMessage( - "ERROR: OnChildRemoved(4) was called an incorrect number of " - "times."); - failed = true; - } - if (listener->num_events("changed 7") != 2) { - LogMessage( - "ERROR: OnChildChanged(7) was called an incorrect number of " - "times."); - failed = true; - } - if (listener->num_events("removed 7") != 1) { - LogMessage( - "ERROR: OnChildRemoved(7) was called an incorrect number of " - "times."); - failed = true; - } - if (listener->total_events() != 9) { - LogMessage("ERROR: ChildListener got an incorrect number of events."); - failed = true; - } - if (!failed) { - LogMessage("SUCCESS: ChildListener got all child events."); - } - delete listener; - } - - // Now check OnDisconnect. When you set an OnDisconnect handler for a - // database location, an operation will be performed on that location when - // you disconnect from Firebase Database. In this sample app, we replicate - // this by shutting down Firebase Database, then starting it up again and - // checking to see if the OnDisconnect actions were performed. - { - LogMessage("TEST: OnDisconnect"); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("SetValueTo1") - .OnDisconnect() - ->SetValue(1), - "OnDisconnectSetValue1"); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("SetValue2Priority3") - .OnDisconnect() - ->SetValueAndPriority(2, 3), - "OnDisconnectSetValue2Priority3"); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("SetValueButThenCancel") - .OnDisconnect() - ->SetValue("Going to cancel this"), - "OnDisconnectSetValueToCancel"); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("SetValueButThenCancel") - .OnDisconnect() - ->Cancel(), - "OnDisconnectCancel"); - // Set a value that we will then remove on disconnect. - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("RemoveValue") - .SetValue("Will be removed"), - "SetValueToRemove"); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("RemoveValue") - .OnDisconnect() - ->RemoveValue(), - "OnDisconnectRemoveValue"); - // Set up a map to pass to OnDisconnect()->UpdateChildren(). - std::map children; - children.insert(std::make_pair("one", 1)); - children.insert(std::make_pair("two", 2)); - children.insert(std::make_pair("three", 3)); - WaitForCompletion(ref.Child("OnDisconnectTests") - .Child("UpdateChildren") - .OnDisconnect() - ->UpdateChildren(children), - "OnDisconnectUpdateChildren"); - LogMessage(" Disconnection handlers registered."); - } - - // Go offline, wait a moment, then go online again. We set up a - // ValueListener - // on one of the OnDisconnect locations we set above, so we can see when the - // disconnection triggers. - { - ExpectValueListener* listener = new ExpectValueListener(1); - ref.Child("OnDisconnectTests") - .Child("SetValueTo1") - .AddValueListener(listener); - - LogMessage(" Disconnecting from Firebase Database."); - database->GoOffline(); - - while (!listener->got_value()) { - ProcessEvents(100); - } - ref.Child("OnDisconnectTests") - .Child("SetValueTo1") - .RemoveValueListener(listener); - delete listener; - listener = nullptr; - - LogMessage(" Reconnecting to Firebase Database."); - database->GoOnline(); - } - - /// Check that the DisconnectionHandler actions were performed. - /// Get a brand new reference to the location to be sure. - ref = database->GetReferenceFromUrl(saved_url.c_str()); - - firebase::Future future = - ref.Child("OnDisconnectTests").GetValue(); - WaitForCompletion(future, "ReadOnDisconnectChanges"); - bool failed = false; - - if (future.error() == firebase::database::kErrorNone) { - const firebase::database::DataSnapshot& result = *future.result(); - if (!result.HasChild("SetValueTo1") || - result.Child("SetValueTo1").value().AsInt64().int64_value() != 1) { - LogMessage("ERROR: OnDisconnect.SetValue(1) failed."); - failed = true; - } - if (!result.HasChild("SetValue2Priority3") || - result.Child("SetValue2Priority3").value().AsInt64().int64_value() != - 2 || - result.Child("SetValue2Priority3").priority().AsInt64().int64_value() != - 3) { - LogMessage("ERROR: OnDisconnect.SetValueAndPriority(2, 3) failed."); - failed = true; - } - if (result.HasChild("RemoveValue")) { - LogMessage("ERROR: OnDisconnect.RemoveValue() failed."); - failed = true; - } - if (result.HasChild("SetValueButThenCancel")) { - LogMessage("ERROR: OnDisconnect.Cancel() failed."); - failed = true; - } - if (!result.HasChild("UpdateChildren") || - !result.Child("UpdateChildren").HasChild("one") || - result.Child("UpdateChildren") - .Child("one") - .value() - .AsInt64() - .int64_value() != 1 || - !result.Child("UpdateChildren").HasChild("two") || - result.Child("UpdateChildren") - .Child("two") - .value() - .AsInt64() - .int64_value() != 2 || - !result.Child("UpdateChildren").HasChild("three") || - result.Child("UpdateChildren") - .Child("three") - .value() - .AsInt64() - .int64_value() != 3) { - LogMessage("ERROR: OnDisconnect.UpdateChildren() failed."); - failed = true; - } - - if (!failed) { - LogMessage("SUCCESS: OnDisconnect values were written properly."); - } - } else { - LogMessage("ERROR: Couldn't read OnDisconnect changes, error %d: %s.", - future.error(), future.error_message()); - } - - bool test_snapshot_was_valid = false; - firebase::database::DataSnapshot* test_snapshot = nullptr; - if (future.error() == firebase::database::kErrorNone) { - // This is a little convoluted as it's not possible to construct an - // empty test snapshot so we copy the result and point at the copy. - static firebase::database::DataSnapshot copied_snapshot = // NOLINT - *future.result(); // NOLINT - test_snapshot = &copied_snapshot; - test_snapshot_was_valid = test_snapshot->is_valid(); - } - - LogMessage("Shutdown the Database library."); - delete database; - database = nullptr; - - // Ensure that the ref we had is now invalid. - if (!ref.is_valid()) { - LogMessage("SUCCESS: Reference was invalidated on library shutdown."); - } else { - LogMessage("ERROR: Reference is still valid after library shutdown."); - } - - if (test_snapshot_was_valid && test_snapshot) { - if (!test_snapshot->is_valid()) { - LogMessage("SUCCESS: Snapshot was invalidated on library shutdown."); - } else { - LogMessage("ERROR: Snapshot is still valid after library shutdown."); - } - } else { - LogMessage( - "WARNING: Snapshot was already invalid at shutdown, couldn't check."); - } - - LogMessage("Signing out from anonymous account."); - auth->SignOut(); - LogMessage("Shutdown the Auth library."); - delete auth; - auth = nullptr; - LogMessage("Shutdown Firebase App."); delete app; From a805e0749b0413eaaa214f6bc2756f7ffa2b62e3 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 14:30:47 -0400 Subject: [PATCH 06/10] setup tests --- functions/testapp/src/common_main.cc | 69 ++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index a30516ca..a586fb1b 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -20,39 +20,90 @@ #include "firebase/future.h" #include "firebase/util.h" +#include +#include + // Thin OS abstraction layer. -#include "main.h" // NOLINT +#include "main.h" // NOLINT // Wait for a Future to be completed. If the Future returns an error, it will // be logged. -void WaitForCompletion(const firebase::FutureBase& future, const char* name) { - while (future.status() == firebase::kFutureStatusPending) { +void WaitForCompletion(const firebase::FutureBase &future, const char *name) +{ + while (future.status() == firebase::kFutureStatusPending) + { ProcessEvents(100); } - if (future.status() != firebase::kFutureStatusComplete) { + if (future.status() != firebase::kFutureStatusComplete) + { LogMessage("ERROR: %s returned an invalid result.", name); - } else if (future.error() != 0) { + } + else if (future.error() != 0) + { LogMessage("ERROR: %s returned error %d: %s", name, future.error(), future.error_message()); } } -extern "C" int common_main(int argc, const char* argv[]) { - ::firebase::App* app; +extern "C" int common_main(int argc, const char *argv[]) +{ + ::firebase::App *app; #if defined(__ANDROID__) app = ::firebase::App::Create(GetJniEnv(), GetActivity()); #else app = ::firebase::App::Create(); -#endif // defined(__ANDROID__) +#endif // defined(__ANDROID__) LogMessage("Initialized Firebase App."); + auto functions = firebase::functions::Functions::GetInstance(app); + + if (functions != nullptr) + { + LogMessage("Initialized Functions."); + + // Setup data to send + auto bool_var = firebase::Variant((bool)true); + auto float_var = firebase::Variant((float)5.0f); + auto int_var = firebase::Variant((int)42); + auto int64_var = firebase::Variant((int64_t)99); + + std::map map_var; + map_var["test_bool"] = bool_var; + map_var["test_float"] = float_var; + map_var["test_int"] = int_var; + map_var["test_int64"] = int64_var; + + std::vector arr_var; + arr_var.push_back(map_var); + arr_var.push_back(bool_var); + arr_var.push_back(float_var); + arr_var.push_back(int_var); + arr_var.push_back(int64_var); + + // The name of the Cloud Function on the server to call + const char *remote_func_name = "echoBody"; // this method echos what is sent + + // The Reference to the remote function + auto func_ref = functions->GetHttpsCallable(remote_func_name); + if (func_ref.is_valid()) + { + LogMessage("Got reference to function: %s", remote_func_name); + } + else + LogMessage("ERROR: %s is not a valid function", remote_func_name); + + } + else + LogMessage("ERROR: starting functions"); + LogMessage("Shutdown Firebase App."); delete app; // Wait until the user wants to quit the app. - while (!ProcessEvents(1000)) { + while (!ProcessEvents(1000)) + { } return 0; From 9657277f26b0467ad7c339ffb60c0908cb8a6eba Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 15:36:59 -0400 Subject: [PATCH 07/10] Working Tests --- functions/testapp/src/common_main.cc | 91 ++++++++++++++++++++++++- functions/testserver/functions/index.js | 10 +++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 functions/testserver/functions/index.js diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index a586fb1b..afe7d5b1 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -20,6 +20,8 @@ #include "firebase/future.h" #include "firebase/util.h" +#include + #include #include @@ -45,6 +47,82 @@ void WaitForCompletion(const firebase::FutureBase &future, const char *name) } } +double GetDoubleVal(const firebase::Variant& val) +{ + if(val.is_double()) return val.double_value(); + if(val.is_int64()) return (double)val.int64_value(); + + return 0.0; +} + +bool CheckDoubleInt64(const firebase::Variant& lhs, const firebase::Variant& rhs) +{ + double left = GetDoubleVal(lhs); + double right = GetDoubleVal(rhs); + + return fabs(left - right) < 0.01; +} + +bool IsEqual(const firebase::Variant& lhs, const firebase::Variant& rhs) +{ + if(lhs.is_numeric() && rhs.is_numeric()) + return CheckDoubleInt64(lhs, rhs); + else if(lhs.is_map() && rhs.is_map()){ + const auto& lhs_map = lhs.map(); + const auto& rhs_map = rhs.map(); + + if(lhs_map.size() != rhs_map.size()) return false; + + for(const auto& kv : lhs_map){ + if(rhs_map.find(kv.first) == rhs_map.end()) + return false; + + if(!IsEqual(rhs_map.at(kv.first), kv.second)) + return false; + } + return true; + } + else if(lhs.is_vector() && rhs.is_vector()){ + const auto& lhs_arr = lhs.vector(); + const auto& rhs_arr = rhs.vector(); + + if(lhs_arr.size() != rhs_arr.size()) + return false; + + for(size_t i = 0; i < lhs_arr.size(); ++i){ + if(!IsEqual(lhs_arr[i], rhs_arr[i])) + return false; + } + } + else if(lhs == rhs) + return true; + return false; +} + +void TestEcho (firebase::functions::HttpsCallableReference& server_func, const firebase::Variant &test_var, const char* test_name) +{ + LogMessage("Starting test [%s]...", test_name); + auto call = server_func.Call(test_var); + WaitForCompletion(call, test_name); + if(call.error() != firebase::functions::kErrorNone){ + LogMessage("[%s] error: %s", test_name, call.error_message()); + return; + } + + if(call.result() == nullptr) + { + LogMessage("[%s] error: missing return value", test_name); + return; + } + + auto result_var = call.result()->data(); + + if(IsEqual(test_var,result_var)) + LogMessage("[%s] : success", test_name); + else + LogMessage("[%s] error: Return value is not equal", test_name); +} + extern "C" int common_main(int argc, const char *argv[]) { ::firebase::App *app; @@ -65,7 +143,7 @@ extern "C" int common_main(int argc, const char *argv[]) // Setup data to send auto bool_var = firebase::Variant((bool)true); - auto float_var = firebase::Variant((float)5.0f); + auto float_var = firebase::Variant((double)5.0f); auto int_var = firebase::Variant((int)42); auto int64_var = firebase::Variant((int64_t)99); @@ -90,6 +168,13 @@ extern "C" int common_main(int argc, const char *argv[]) if (func_ref.is_valid()) { LogMessage("Got reference to function: %s", remote_func_name); + + TestEcho(func_ref, bool_var, "bool"); + TestEcho(func_ref, float_var, "float"); + TestEcho(func_ref, int_var, "int"); + TestEcho(func_ref, int64_var, "int64"); + TestEcho(func_ref, map_var, "map"); + TestEcho(func_ref, arr_var, "array"); } else LogMessage("ERROR: %s is not a valid function", remote_func_name); @@ -98,6 +183,10 @@ extern "C" int common_main(int argc, const char *argv[]) else LogMessage("ERROR: starting functions"); + + LogMessage("Shutdown Functions."); + delete functions; + LogMessage("Shutdown Firebase App."); delete app; diff --git a/functions/testserver/functions/index.js b/functions/testserver/functions/index.js new file mode 100644 index 00000000..381b3a4e --- /dev/null +++ b/functions/testserver/functions/index.js @@ -0,0 +1,10 @@ +const functions = require('firebase-functions'); + +/////////////////////////////////////////////////////////////////////// +// debug callback for testing Cloud Functions Calls +exports.echoBody = functions.https.onRequest((request, response) => { + // Whatever you do, DO NOT SEND response UNTIL you have finished your work. + // Wait on Promises with 'then' clauses if you need to. + response.status(200); + response.json(request.body); +}); \ No newline at end of file From b446971f4d7c16008d9faf30fe318f2777d7ee6a Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 15:45:08 -0400 Subject: [PATCH 08/10] formatting --- functions/testapp/src/common_main.cc | 136 ++++++++++++++------------- 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/functions/testapp/src/common_main.cc b/functions/testapp/src/common_main.cc index afe7d5b1..f91851ad 100644 --- a/functions/testapp/src/common_main.cc +++ b/functions/testapp/src/common_main.cc @@ -4,7 +4,7 @@ // 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 +// 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, @@ -43,84 +43,92 @@ void WaitForCompletion(const firebase::FutureBase &future, const char *name) else if (future.error() != 0) { LogMessage("ERROR: %s returned error %d: %s", name, future.error(), - future.error_message()); + future.error_message()); } } -double GetDoubleVal(const firebase::Variant& val) +double GetDoubleVal(const firebase::Variant &val) { - if(val.is_double()) return val.double_value(); - if(val.is_int64()) return (double)val.int64_value(); + if (val.is_double()) + return val.double_value(); + if (val.is_int64()) + return (double)val.int64_value(); - return 0.0; + return 0.0; } -bool CheckDoubleInt64(const firebase::Variant& lhs, const firebase::Variant& rhs) +bool CheckDoubleInt64(const firebase::Variant &lhs, const firebase::Variant &rhs) { - double left = GetDoubleVal(lhs); - double right = GetDoubleVal(rhs); + double left = GetDoubleVal(lhs); + double right = GetDoubleVal(rhs); - return fabs(left - right) < 0.01; + return fabs(left - right) < 0.01; } -bool IsEqual(const firebase::Variant& lhs, const firebase::Variant& rhs) +bool IsEqual(const firebase::Variant &lhs, const firebase::Variant &rhs) { - if(lhs.is_numeric() && rhs.is_numeric()) - return CheckDoubleInt64(lhs, rhs); - else if(lhs.is_map() && rhs.is_map()){ - const auto& lhs_map = lhs.map(); - const auto& rhs_map = rhs.map(); - - if(lhs_map.size() != rhs_map.size()) return false; - - for(const auto& kv : lhs_map){ - if(rhs_map.find(kv.first) == rhs_map.end()) - return false; - - if(!IsEqual(rhs_map.at(kv.first), kv.second)) - return false; - } - return true; + if (lhs.is_numeric() && rhs.is_numeric()) + return CheckDoubleInt64(lhs, rhs); + else if (lhs.is_map() && rhs.is_map()) + { + const auto &lhs_map = lhs.map(); + const auto &rhs_map = rhs.map(); + + if (lhs_map.size() != rhs_map.size()) + return false; + + for (const auto &kv : lhs_map) + { + if (rhs_map.find(kv.first) == rhs_map.end()) + return false; + + if (!IsEqual(rhs_map.at(kv.first), kv.second)) + return false; } - else if(lhs.is_vector() && rhs.is_vector()){ - const auto& lhs_arr = lhs.vector(); - const auto& rhs_arr = rhs.vector(); + return true; + } + else if (lhs.is_vector() && rhs.is_vector()) + { + const auto &lhs_arr = lhs.vector(); + const auto &rhs_arr = rhs.vector(); - if(lhs_arr.size() != rhs_arr.size()) - return false; + if (lhs_arr.size() != rhs_arr.size()) + return false; - for(size_t i = 0; i < lhs_arr.size(); ++i){ - if(!IsEqual(lhs_arr[i], rhs_arr[i])) - return false; - } + for (size_t i = 0; i < lhs_arr.size(); ++i) + { + if (!IsEqual(lhs_arr[i], rhs_arr[i])) + return false; } - else if(lhs == rhs) - return true; - return false; + } + else if (lhs == rhs) + return true; + return false; } -void TestEcho (firebase::functions::HttpsCallableReference& server_func, const firebase::Variant &test_var, const char* test_name) +void TestEcho(firebase::functions::HttpsCallableReference &server_func, const firebase::Variant &test_var, const char *test_name) { - LogMessage("Starting test [%s]...", test_name); - auto call = server_func.Call(test_var); - WaitForCompletion(call, test_name); - if(call.error() != firebase::functions::kErrorNone){ - LogMessage("[%s] error: %s", test_name, call.error_message()); - return; - } + LogMessage("Starting test [%s]...", test_name); + auto call = server_func.Call(test_var); + WaitForCompletion(call, test_name); + if (call.error() != firebase::functions::kErrorNone) + { + LogMessage("[%s] error: %s", test_name, call.error_message()); + return; + } - if(call.result() == nullptr) - { - LogMessage("[%s] error: missing return value", test_name); - return; - } + if (call.result() == nullptr) + { + LogMessage("[%s] error: missing return value", test_name); + return; + } - auto result_var = call.result()->data(); + auto result_var = call.result()->data(); - if(IsEqual(test_var,result_var)) - LogMessage("[%s] : success", test_name); - else - LogMessage("[%s] error: Return value is not equal", test_name); + if (IsEqual(test_var, result_var)) + LogMessage("[%s] : success", test_name); + else + LogMessage("[%s] error: Return value is not equal", test_name); } extern "C" int common_main(int argc, const char *argv[]) @@ -169,21 +177,19 @@ extern "C" int common_main(int argc, const char *argv[]) { LogMessage("Got reference to function: %s", remote_func_name); - TestEcho(func_ref, bool_var, "bool"); - TestEcho(func_ref, float_var, "float"); - TestEcho(func_ref, int_var, "int"); - TestEcho(func_ref, int64_var, "int64"); - TestEcho(func_ref, map_var, "map"); - TestEcho(func_ref, arr_var, "array"); + TestEcho(func_ref, bool_var, "bool"); + TestEcho(func_ref, float_var, "float"); + TestEcho(func_ref, int_var, "int"); + TestEcho(func_ref, int64_var, "int64"); + TestEcho(func_ref, map_var, "map"); + TestEcho(func_ref, arr_var, "array"); } else LogMessage("ERROR: %s is not a valid function", remote_func_name); - } else LogMessage("ERROR: starting functions"); - LogMessage("Shutdown Functions."); delete functions; From a5d61899d72367d66cc56175de0bd8e8682279b6 Mon Sep 17 00:00:00 2001 From: JPoag Date: Fri, 3 Aug 2018 16:07:08 -0400 Subject: [PATCH 09/10] Notes --- functions/testapp/Podfile | 2 +- functions/testapp/readme.md | 13 ++++++++- functions/testserver/readme.md | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 functions/testserver/readme.md diff --git a/functions/testapp/Podfile b/functions/testapp/Podfile index f882b11a..71037b25 100644 --- a/functions/testapp/Podfile +++ b/functions/testapp/Podfile @@ -3,6 +3,6 @@ platform :ios, '8.0' # Firebase Cloud Functions test application. target 'testapp' do pod 'Firebase/Core' - pod 'Firebase/functions' + pod 'Firebase/Functions' pod 'Firebase/Auth' end diff --git a/functions/testapp/readme.md b/functions/testapp/readme.md index 221564e7..44a68b33 100644 --- a/functions/testapp/readme.md +++ b/functions/testapp/readme.md @@ -6,12 +6,23 @@ Cloud Functions operations with the Firebase Cloud Functions C++ SDK. The application has no user interface and simply logs actions it's performing to the console. +There are **two parts** to this project, the app and the server. Without the server, the app won't work correctly. However, you need to follow all of these instructions to setup your Firebase Project before you can setup the server. + +Follow the directions in `testserver\readme.md` to upload the echo function to the server. + The testapp performs the following: - Creates a firebase::App in a platform-specific way. The App holds platform-specific context that's used by other Firebase APIs, and is a central point for communication between the Firebase Cloud Functions C++ and Firebase Auth C++ libraries. - - Shuts down the Firebase functions, Firebase Auth, and Firebase App systems. + - Creates a Functions Object + - Grabs a HTTPS function reference to 'echoBody' + - Sends various values to server, and checks that the echo response + is the same as what was sent. + - Shuts down the Firebase functions and Firebase App systems. + +The testserver performs the following: + - Echos the request body back as a JSON response. Introduction ------------ diff --git a/functions/testserver/readme.md b/functions/testserver/readme.md new file mode 100644 index 00000000..08e0bf59 --- /dev/null +++ b/functions/testserver/readme.md @@ -0,0 +1,48 @@ +Firebase Cloud Functions Quickstart +======================== + +The Firebase Cloud Functions Test Application (testapp) demonstrates Firebase +Cloud Functions operations with the Firebase Cloud Functions C++ SDK. The +application has no user interface and simply logs actions it's performing to the +console. + +The testapp performs the following: + - Creates a firebase::App in a platform-specific way. The App holds + platform-specific context that's used by other Firebase APIs, and is a + central point for communication between the Firebase Cloud Functions C++ + and Firebase Auth C++ libraries. + - Creates a Functions Object + - Grabs a HTTPS function reference to 'echoBody' + - Sends various values to server, and checks that the echo response + is the same as what was sent. + - Shuts down the Firebase functions and Firebase App systems. + +The testserver performs the following: + - Echos the request body back as a JSON response. + +Requirements +------------ + +- [Please make sure that the Firebase CLI is installed on your machine](https://firebase.google.com/docs/cli/) + + +Building and Running the testsever +-------------------------------- + +First, create an empty folder. From the command shell, execute the following: + +``` +$ firebase init +``` + +Follow the prompts and initialize a new Firebase project with support for Functions. Be sure to select the same project that you used for testapp. (See testapp/readme.md) + +Copy the `testserver\functions\index.js` to the `functions\index.js` in your project folder. This will overwrite your current (empty) file. + +Run the following command to deploy the functions: + +``` +$ firebase deploy +``` + +Read the `functions\index.js` file that you *just* deployed to make sure everything looks kosher. \ No newline at end of file From 655401dadee09f182ddc023e09eec196acf55b6c Mon Sep 17 00:00:00 2001 From: JPoag Date: Thu, 30 Aug 2018 07:28:33 -0400 Subject: [PATCH 10/10] Switch Server Echo handler from `onRequest` to `onCall` --- functions/testserver/functions/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/functions/testserver/functions/index.js b/functions/testserver/functions/index.js index 381b3a4e..476651c8 100644 --- a/functions/testserver/functions/index.js +++ b/functions/testserver/functions/index.js @@ -2,9 +2,8 @@ const functions = require('firebase-functions'); /////////////////////////////////////////////////////////////////////// // debug callback for testing Cloud Functions Calls -exports.echoBody = functions.https.onRequest((request, response) => { - // Whatever you do, DO NOT SEND response UNTIL you have finished your work. +exports.echoBody = functions.https.onCall((data, context) => { + // Whatever you do, DO NOT RETURN response UNTIL you have finished your work. // Wait on Promises with 'then' clauses if you need to. - response.status(200); - response.json(request.body); + return data; }); \ No newline at end of file