diff --git a/Android/firebase_dependencies.gradle b/Android/firebase_dependencies.gradle index 232026a2b1..1ae4a4674d 100644 --- a/Android/firebase_dependencies.gradle +++ b/Android/firebase_dependencies.gradle @@ -27,7 +27,8 @@ def firebaseDependenciesMap = [ 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links'], 'firestore' : ['com.google.firebase:firebase-firestore'], 'functions' : ['com.google.firebase:firebase-functions'], - 'gma' : ['com.google.android.gms:play-services-ads:22.3.0'], + 'gma' : ['com.google.android.gms:play-services-ads:22.3.0', + 'com.google.android.ump:user-messaging-platform:2.1.0'], 'installations' : ['com.google.firebase:firebase-installations'], 'invites' : ['com.google.firebase:firebase-invites'], // Messaging has an additional local dependency to include. diff --git a/gma/CMakeLists.txt b/gma/CMakeLists.txt index 887139f1db..a58354f7f9 100644 --- a/gma/CMakeLists.txt +++ b/gma/CMakeLists.txt @@ -40,7 +40,7 @@ binary_to_array("gma_resources" # Source files used by the Android implementation. set(android_SRCS ${gma_resources_source} - src/stub/ump/consent_info_internal_stub.cc + src/android/ump/consent_info_internal_android.cc src/android/ad_request_converter.cc src/android/ad_error_android.cc src/android/adapter_response_info_android.cc diff --git a/gma/gma_resources/build.gradle b/gma/gma_resources/build.gradle index 735d2291cc..d79977e4a0 100644 --- a/gma/gma_resources/build.gradle +++ b/gma/gma_resources/build.gradle @@ -32,6 +32,11 @@ allprojects { apply plugin: 'com.android.library' android { + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + compileSdkVersion 28 sourceSets { @@ -48,6 +53,7 @@ dependencies { implementation platform('com.google.firebase:firebase-bom:32.2.3') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.android.gms:play-services-ads:22.3.0' + implementation 'com.google.android.ump:user-messaging-platform:2.1.0' } afterEvaluate { diff --git a/gma/integration_test/build.gradle b/gma/integration_test/build.gradle index bbbfd2577d..fd65c3885e 100644 --- a/gma/integration_test/build.gradle +++ b/gma/integration_test/build.gradle @@ -73,6 +73,9 @@ android { proguardFile file('proguard.pro') } } + lintOptions { + abortOnError false + } } apply from: "$gradle.firebase_cpp_sdk_dir/Android/firebase_dependencies.gradle" diff --git a/gma/integration_test/src/integration_test.cc b/gma/integration_test/src/integration_test.cc index da158fa0ea..b51a96d3af 100644 --- a/gma/integration_test/src/integration_test.cc +++ b/gma/integration_test/src/integration_test.cc @@ -268,6 +268,7 @@ void FirebaseGmaTest::SetUp() { // debugging. They appear as a long string of hex characters. firebase::gma::RequestConfiguration request_configuration; request_configuration.test_device_ids = kTestDeviceIDs; + request_configuration.test_device_ids.push_back(GetDebugDeviceId()); firebase::gma::SetRequestConfiguration(request_configuration); } @@ -332,6 +333,7 @@ void FirebaseGmaUITest::SetUp() { // debugging. They appear as a long string of hex characters. firebase::gma::RequestConfiguration request_configuration; request_configuration.test_device_ids = kTestDeviceIDs; + request_configuration.test_device_ids.push_back(GetDebugDeviceId()); firebase::gma::SetRequestConfiguration(request_configuration); } @@ -2493,7 +2495,12 @@ class FirebaseGmaUmpTest : public FirebaseGmaTest { void FirebaseGmaUmpTest::InitializeUmp(ResetOption reset) { using firebase::gma::ump::ConsentInfo; - consent_info_ = ConsentInfo::GetInstance(*shared_app_); + firebase::InitResult result; + consent_info_ = ConsentInfo::GetInstance(*shared_app_, &result); + + EXPECT_NE(consent_info_, nullptr); + EXPECT_EQ(result, firebase::kInitResultSuccess); + if (consent_info_ != nullptr && reset == kReset) { consent_info_->Reset(); } @@ -2937,4 +2944,90 @@ TEST_F(FirebaseGmaUmpTest, TestUmpCleanupRaceCondition) { EXPECT_EQ(future_show.status(), firebase::kFutureStatusInvalid); } +TEST_F(FirebaseGmaUmpTest, TestUmpMethodsReturnOperationInProgress) { + SKIP_TEST_ON_DESKTOP; + + using firebase::gma::ump::ConsentFormStatus; + using firebase::gma::ump::ConsentRequestParameters; + using firebase::gma::ump::ConsentStatus; + + // Check that all of the UMP operations properly return an OperationInProgress + // error if called more than once at the same time. Each step of this test is + // inherently flaky, so add flaky test blocks all over. + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + ShouldRunUITests() ? firebase::gma::ump::kConsentDebugGeographyEEA + : firebase::gma::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + FLAKY_TEST_SECTION_BEGIN(); + firebase::Future future_request_1 = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_request_2 = + consent_info_->RequestConsentInfoUpdate(params); + WaitForCompletion( + future_request_2, "RequestConsentInfoUpdate second", + firebase::gma::ump::kConsentRequestErrorOperationInProgress); + WaitForCompletion(future_request_1, "RequestConsentInfoUpdate first"); + FLAKY_TEST_SECTION_END(); + + if (ShouldRunUITests()) { + // The below should only be checked if UI tests are enabled, as they + // require some interaction. + FLAKY_TEST_SECTION_BEGIN(); + firebase::Future future_load_1 = consent_info_->LoadConsentForm(); + firebase::Future future_load_2 = consent_info_->LoadConsentForm(); + WaitForCompletion(future_load_2, "LoadConsentForm second", + firebase::gma::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_load_1, "LoadConsentForm first"); + FLAKY_TEST_SECTION_END(); + + FLAKY_TEST_SECTION_BEGIN(); + firebase::Future future_show_1 = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + firebase::Future future_show_2 = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + WaitForCompletion(future_show_2, "ShowConsentForm second", + firebase::gma::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_show_1, "ShowConsentForm first"); + FLAKY_TEST_SECTION_END(); + + FLAKY_TEST_SECTION_BEGIN(); + firebase::Future future_privacy_1 = + consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + firebase::Future future_privacy_2 = + consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + WaitForCompletion(future_privacy_2, "ShowPrivacyOptionsForm second", + firebase::gma::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_privacy_1, "ShowPrivacyOptionsForm first"); + FLAKY_TEST_SECTION_END(); + + consent_info_->Reset(); + // Request again so we can test LoadAndShowConsentFormIfRequired. + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + FLAKY_TEST_SECTION_BEGIN(); + firebase::Future future_load_and_show_1 = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + firebase::Future future_load_and_show_2 = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + WaitForCompletion(future_load_and_show_2, + "LoadAndShowConsentFormIfRequired second", + firebase::gma::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_load_and_show_1, + "LoadAndShowConsentFormIfRequired first"); + FLAKY_TEST_SECTION_END(); + } else { + LogInfo("Skipping methods that require user interaction."); + } +} + } // namespace firebase_testapp_automated diff --git a/gma/src/android/gma_android.cc b/gma/src/android/gma_android.cc index bcc7e35949..8ce19c7295 100644 --- a/gma/src/android/gma_android.cc +++ b/gma/src/android/gma_android.cc @@ -49,6 +49,12 @@ namespace firebase { namespace gma { +namespace internal { +::firebase::Mutex g_cached_gma_embedded_files_mutex; +std::vector<::firebase::internal::EmbeddedFile>* g_cached_gma_embedded_files = + nullptr; +} // namespace internal + METHOD_LOOKUP_DEFINITION(mobile_ads, PROGUARD_KEEP_CLASS "com/google/android/gms/ads/MobileAds", @@ -308,12 +314,22 @@ Future Initialize(JNIEnv* env, jobject activity, return Future(); } - const std::vector embedded_files = - util::CacheEmbeddedFiles(env, activity, - firebase::internal::EmbeddedFile::ToVector( - firebase_gma::gma_resources_filename, - firebase_gma::gma_resources_data, - firebase_gma::gma_resources_size)); + // Between this and UMP, we only want to load these files once. + { + MutexLock lock(internal::g_cached_gma_embedded_files_mutex); + if (internal::g_cached_gma_embedded_files == nullptr) { + internal::g_cached_gma_embedded_files = + new std::vector(); + *internal::g_cached_gma_embedded_files = + util::CacheEmbeddedFiles(env, activity, + firebase::internal::EmbeddedFile::ToVector( + firebase_gma::gma_resources_filename, + firebase_gma::gma_resources_data, + firebase_gma::gma_resources_size)); + } + } + const std::vector& embedded_files = + *internal::g_cached_gma_embedded_files; if (!(mobile_ads::CacheMethodIds(env, activity) && ad_request_builder::CacheMethodIds(env, activity) && diff --git a/gma/src/android/gma_android.h b/gma/src/android/gma_android.h index f2a38f9d21..8ada58c3d2 100644 --- a/gma/src/android/gma_android.h +++ b/gma/src/android/gma_android.h @@ -19,7 +19,11 @@ #include +#include + +#include "app/src/embedded_file.h" #include "app/src/util_android.h" +#include "firebase/internal/mutex.h" #include "gma/src/common/gma_common.h" namespace firebase { @@ -189,6 +193,14 @@ void ReleaseClasses(JNIEnv* env); jobject CreateJavaAdSize(JNIEnv* env, jobject activity, const AdSize& an_ad_size); +namespace internal { +// GMA and UMP share embedded dex files; this ensures +// that they are only loaded once each run. +extern ::firebase::Mutex g_cached_gma_embedded_files_mutex; +extern std::vector<::firebase::internal::EmbeddedFile>* + g_cached_gma_embedded_files; +} // namespace internal + } // namespace gma } // namespace firebase diff --git a/gma/src/android/ump/consent_info_internal_android.cc b/gma/src/android/ump/consent_info_internal_android.cc new file mode 100644 index 0000000000..903b7126cd --- /dev/null +++ b/gma/src/android/ump/consent_info_internal_android.cc @@ -0,0 +1,668 @@ +/* + * Copyright 2023 Google LLC + * + * 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 "gma/src/android/ump/consent_info_internal_android.h" + +#include + +#include +#include + +#include "app/src/assert.h" +#include "app/src/thread.h" +#include "app/src/util_android.h" +#include "firebase/internal/common.h" +#include "gma/gma_resources.h" +#include "gma/src/android/gma_android.h" + +namespace firebase { +namespace gma { +namespace ump { +namespace internal { + +ConsentInfoInternalAndroid* ConsentInfoInternalAndroid::s_instance = nullptr; +firebase::Mutex ConsentInfoInternalAndroid::s_instance_mutex; + +// clang-format off +#define CONSENTINFOHELPER_METHODS(X) \ + X(Constructor, "", "(JLandroid/app/Activity;)V"), \ + X(GetConsentStatus, "getConsentStatus", "()I"), \ + X(RequestConsentInfoUpdate, "requestConsentInfoUpdate", \ + "(JZILjava/util/ArrayList;)V"), \ + X(LoadConsentForm, "loadConsentForm", "(J)V"), \ + X(ShowConsentForm, "showConsentForm", "(JLandroid/app/Activity;)Z"), \ + X(LoadAndShowConsentFormIfRequired, "loadAndShowConsentFormIfRequired", \ + "(JLandroid/app/Activity;)V"), \ + X(GetPrivacyOptionsRequirementStatus, "getPrivacyOptionsRequirementStatus", \ + "()I"), \ + X(ShowPrivacyOptionsForm, "showPrivacyOptionsForm", \ + "(JLandroid/app/Activity;)V"), \ + X(Reset, "reset", "()V"), \ + X(CanRequestAds, "canRequestAds", "()Z"), \ + X(IsConsentFormAvailable, "isConsentFormAvailable", "()Z"), \ + X(Disconnect, "disconnect", "()V") +// clang-format on + +// clang-format off +#define CONSENTINFOHELPER_FIELDS(X) \ + X(PrivacyOptionsRequirementUnknown, \ + "PRIVACY_OPTIONS_REQUIREMENT_UNKNOWN", "I", util::kFieldTypeStatic), \ + X(PrivacyOptionsRequirementRequired, \ + "PRIVACY_OPTIONS_REQUIREMENT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(PrivacyOptionsRequirementNotRequired, \ + "PRIVACY_OPTIONS_REQUIREMENT_NOT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(FunctionRequestConsentInfoUpdate, \ + "FUNCTION_REQUEST_CONSENT_INFO_UPDATE", "I", util::kFieldTypeStatic), \ + X(FunctionLoadConsentForm, \ + "FUNCTION_LOAD_CONSENT_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionShowConsentForm, \ + "FUNCTION_SHOW_CONSENT_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionLoadAndShowConsentFormIfRequired, \ + "FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED", \ + "I", util::kFieldTypeStatic), \ + X(FunctionShowPrivacyOptionsForm, \ + "FUNCTION_SHOW_PRIVACY_OPTIONS_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionCount, "FUNCTION_COUNT", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consent_info_helper, CONSENTINFOHELPER_METHODS, + CONSENTINFOHELPER_FIELDS); + +METHOD_LOOKUP_DEFINITION( + consent_info_helper, + "com/google/firebase/gma/internal/cpp/ConsentInfoHelper", + CONSENTINFOHELPER_METHODS, CONSENTINFOHELPER_FIELDS); + +// clang-format off +#define CONSENTINFORMATION_CONSENTSTATUS_FIELDS(X) \ + X(Unknown, "UNKNOWN", "I", util::kFieldTypeStatic), \ + X(NotRequired, "NOT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(Required, "REQUIRED", "I", util::kFieldTypeStatic), \ + X(Obtained, "OBTAINED", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consentinformation_consentstatus, METHOD_LOOKUP_NONE, + CONSENTINFORMATION_CONSENTSTATUS_FIELDS); +METHOD_LOOKUP_DEFINITION( + consentinformation_consentstatus, + PROGUARD_KEEP_CLASS + "com/google/android/ump/ConsentInformation$ConsentStatus", + METHOD_LOOKUP_NONE, CONSENTINFORMATION_CONSENTSTATUS_FIELDS); + +// clang-format off +#define FORMERROR_ERRORCODE_FIELDS(X) \ + X(InternalError, "INTERNAL_ERROR", "I", util::kFieldTypeStatic), \ + X(InternetError, "INTERNET_ERROR", "I", util::kFieldTypeStatic), \ + X(InvalidOperation, "INVALID_OPERATION", "I", util::kFieldTypeStatic), \ + X(TimeOut, "TIME_OUT", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(formerror_errorcode, METHOD_LOOKUP_NONE, + FORMERROR_ERRORCODE_FIELDS); +METHOD_LOOKUP_DEFINITION(formerror_errorcode, + PROGUARD_KEEP_CLASS + "com/google/android/ump/FormError$ErrorCode", + METHOD_LOOKUP_NONE, FORMERROR_ERRORCODE_FIELDS); + +// clang-format off +#define CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS(X) \ + X(Disabled, "DEBUG_GEOGRAPHY_DISABLED", "I", util::kFieldTypeStatic), \ + X(EEA, "DEBUG_GEOGRAPHY_EEA", "I", util::kFieldTypeStatic), \ + X(NotEEA, "DEBUG_GEOGRAPHY_NOT_EEA", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consentdebugsettings_debuggeography, + METHOD_LOOKUP_NONE, + CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS); +METHOD_LOOKUP_DEFINITION( + consentdebugsettings_debuggeography, + PROGUARD_KEEP_CLASS + "com/google/android/ump/ConsentDebugSettings$DebugGeography", + METHOD_LOOKUP_NONE, CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS); + +// This explicitly implements the constructor for the outer class, +// ConsentInfoInternal. +ConsentInfoInternal* ConsentInfoInternal::CreateInstance(JNIEnv* jni_env, + jobject activity) { + ConsentInfoInternalAndroid* ptr = + new ConsentInfoInternalAndroid(jni_env, activity); + if (!ptr->valid()) { + delete ptr; + return nullptr; + } + return ptr; +} + +static void ReleaseClasses(JNIEnv* env) { + consent_info_helper::ReleaseClass(env); + consentinformation_consentstatus::ReleaseClass(env); + formerror_errorcode::ReleaseClass(env); + consentdebugsettings_debuggeography::ReleaseClass(env); +} + +ConsentInfoInternalAndroid::~ConsentInfoInternalAndroid() { + JNIEnv* env = GetJNIEnv(); + env->CallVoidMethod(helper_, consent_info_helper::GetMethodId( + consent_info_helper::kDisconnect)); + + MutexLock lock(s_instance_mutex); + s_instance = nullptr; + + env->DeleteGlobalRef(helper_); + helper_ = nullptr; + + ReleaseClasses(env); + util::Terminate(env); + + env->DeleteGlobalRef(activity_); + activity_ = nullptr; + java_vm_ = nullptr; +} + +// clang-format off +#define ENUM_VALUE(class_namespace, field_name) \ + env->GetStaticIntField(class_namespace::GetClass(), \ + class_namespace::GetFieldId(class_namespace::k##field_name)) +// clang-format on + +void ConsentInfoInternalAndroid::CacheEnumValues(JNIEnv* env) { + // Cache enum values when the class loads, to avoid JNI lookups during + // callbacks later on when converting enums between Android and C++ values. + enums_.consentstatus_unknown = + ENUM_VALUE(consentinformation_consentstatus, Unknown); + enums_.consentstatus_required = + ENUM_VALUE(consentinformation_consentstatus, Required); + enums_.consentstatus_not_required = + ENUM_VALUE(consentinformation_consentstatus, NotRequired); + enums_.consentstatus_obtained = + ENUM_VALUE(consentinformation_consentstatus, Obtained); + + enums_.debug_geography_disabled = + ENUM_VALUE(consentdebugsettings_debuggeography, Disabled); + enums_.debug_geography_eea = + ENUM_VALUE(consentdebugsettings_debuggeography, EEA); + enums_.debug_geography_not_eea = + ENUM_VALUE(consentdebugsettings_debuggeography, NotEEA); + + enums_.formerror_success = 0; + enums_.formerror_internal = ENUM_VALUE(formerror_errorcode, InternalError); + enums_.formerror_network = ENUM_VALUE(formerror_errorcode, InternetError); + enums_.formerror_invalid_operation = + ENUM_VALUE(formerror_errorcode, InvalidOperation); + enums_.formerror_timeout = ENUM_VALUE(formerror_errorcode, TimeOut); + + enums_.privacy_options_requirement_unknown = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementUnknown); + enums_.privacy_options_requirement_required = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementRequired); + enums_.privacy_options_requirement_not_required = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementNotRequired); + + enums_.function_request_consent_info_update = + ENUM_VALUE(consent_info_helper, FunctionRequestConsentInfoUpdate); + enums_.function_load_consent_form = + ENUM_VALUE(consent_info_helper, FunctionLoadConsentForm); + enums_.function_show_consent_form = + ENUM_VALUE(consent_info_helper, FunctionShowConsentForm); + enums_.function_load_and_show_consent_form_if_required = + ENUM_VALUE(consent_info_helper, FunctionLoadAndShowConsentFormIfRequired); + enums_.function_show_privacy_options_form = + ENUM_VALUE(consent_info_helper, FunctionShowPrivacyOptionsForm); + enums_.function_count = ENUM_VALUE(consent_info_helper, FunctionCount); +} + +void ConsentInfoInternalAndroid::JNI_ConsentInfoHelper_completeFuture( + JNIEnv* env, jclass clazz, jint future_fn, jlong consent_info_internal_ptr, + jlong future_handle, jint error_code, jobject error_message_obj) { + MutexLock lock(s_instance_mutex); + if (consent_info_internal_ptr == 0 || s_instance == nullptr) { + // Calling this with a null pointer, or if there is no active + // instance, is a no-op, so just return. + return; + } + ConsentInfoInternalAndroid* instance = + reinterpret_cast(consent_info_internal_ptr); + if (s_instance != instance) { + // If the instance we were called with does not match the current + // instance, a bad race condition has occurred (whereby while waiting for + // the operation to complete, ConsentInfo was deleted and then recreated). + // In that case, fully ignore this callback. + return; + } + std::string error_message = + error_message_obj ? util::JniStringToString(env, error_message_obj) : ""; + instance->CompleteFutureFromJniCallback( + env, future_fn, static_cast(future_handle), + static_cast(error_code), + error_message.length() > 0 ? error_message.c_str() : nullptr); +} + +ConsentInfoInternalAndroid::ConsentInfoInternalAndroid(JNIEnv* env, + jobject activity) + : java_vm_(nullptr), + activity_(nullptr), + helper_(nullptr), + has_requested_consent_info_update_(false) { + MutexLock lock(s_instance_mutex); + FIREBASE_ASSERT(s_instance == nullptr); + s_instance = this; + + util::Initialize(env, activity); + env->GetJavaVM(&java_vm_); + + // Between this and GMA, we only want to load these files once. + { + MutexLock lock( + ::firebase::gma::internal::g_cached_gma_embedded_files_mutex); + if (::firebase::gma::internal::g_cached_gma_embedded_files == nullptr) { + ::firebase::gma::internal::g_cached_gma_embedded_files = + new std::vector(); + *::firebase::gma::internal::g_cached_gma_embedded_files = + util::CacheEmbeddedFiles(env, activity, + firebase::internal::EmbeddedFile::ToVector( + firebase_gma::gma_resources_filename, + firebase_gma::gma_resources_data, + firebase_gma::gma_resources_size)); + } + } + const std::vector& embedded_files = + *::firebase::gma::internal::g_cached_gma_embedded_files; + + if (!(consent_info_helper::CacheClassFromFiles(env, activity, + &embedded_files) != nullptr && + consent_info_helper::CacheMethodIds(env, activity) && + consent_info_helper::CacheFieldIds(env, activity) && + consentinformation_consentstatus::CacheFieldIds(env, activity) && + formerror_errorcode::CacheFieldIds(env, activity) && + consentdebugsettings_debuggeography::CacheFieldIds(env, activity))) { + ReleaseClasses(env); + util::Terminate(env); + return; + } + static const JNINativeMethod kConsentInfoHelperNativeMethods[] = { + {"completeFuture", "(IJJILjava/lang/String;)V", + reinterpret_cast(&JNI_ConsentInfoHelper_completeFuture)}}; + if (!consent_info_helper::RegisterNatives( + env, kConsentInfoHelperNativeMethods, + FIREBASE_ARRAYSIZE(kConsentInfoHelperNativeMethods))) { + util::CheckAndClearJniExceptions(env); + ReleaseClasses(env); + util::Terminate(env); + return; + } + util::CheckAndClearJniExceptions(env); + jobject helper_ref = env->NewObject( + consent_info_helper::GetClass(), + consent_info_helper::GetMethodId(consent_info_helper::kConstructor), + reinterpret_cast(this), activity); + util::CheckAndClearJniExceptions(env); + if (!helper_ref) { + ReleaseClasses(env); + util::Terminate(env); + return; + } + + helper_ = env->NewGlobalRef(helper_ref); + FIREBASE_ASSERT(helper_); + env->DeleteLocalRef(helper_ref); + + activity_ = env->NewGlobalRef(activity); + + util::CheckAndClearJniExceptions(env); + + CacheEnumValues(env); + + util::CheckAndClearJniExceptions(env); +} + +ConsentStatus ConsentInfoInternalAndroid::CppConsentStatusFromAndroid( + jint status) { + if (status == enums().consentstatus_unknown) return kConsentStatusUnknown; + if (status == enums().consentstatus_required) return kConsentStatusRequired; + if (status == enums().consentstatus_not_required) + return kConsentStatusNotRequired; + if (status == enums().consentstatus_obtained) return kConsentStatusObtained; + LogWarning("GMA: Unknown ConsentStatus returned by UMP Android SDK: %d", + (int)status); + return kConsentStatusUnknown; +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalAndroid::CppPrivacyOptionsRequirementStatusFromAndroid( + jint status) { + if (status == enums().privacy_options_requirement_unknown) + return kPrivacyOptionsRequirementStatusUnknown; + if (status == enums().privacy_options_requirement_required) + return kPrivacyOptionsRequirementStatusRequired; + if (status == enums().privacy_options_requirement_not_required) + return kPrivacyOptionsRequirementStatusNotRequired; + LogWarning( + "GMA: Unknown PrivacyOptionsRequirementStatus returned by UMP Android " + "SDK: %d", + (int)status); + return kPrivacyOptionsRequirementStatusUnknown; +} + +jint ConsentInfoInternalAndroid::AndroidDebugGeographyFromCppDebugGeography( + ConsentDebugGeography geo) { + switch (geo) { + case kConsentDebugGeographyDisabled: + return enums().debug_geography_disabled; + case kConsentDebugGeographyEEA: + return enums().debug_geography_eea; + case kConsentDebugGeographyNonEEA: + return enums().debug_geography_not_eea; + default: + return enums().debug_geography_disabled; + } +} + +// Android uses FormError to report request errors as well. +ConsentRequestError +ConsentInfoInternalAndroid::CppConsentRequestErrorFromAndroidFormError( + jint error, const char* message) { + if (error == enums().formerror_success) return kConsentRequestSuccess; + if (error == enums().formerror_internal) return kConsentRequestErrorInternal; + if (error == enums().formerror_network) return kConsentRequestErrorNetwork; + if (error == enums().formerror_invalid_operation) { + // Error strings taken directly from the UMP Android SDK. + if (message && strcasestr(message, "misconfiguration") != nullptr) + return kConsentRequestErrorMisconfiguration; + else if (message && + strcasestr(message, "requires a valid application ID") != nullptr) + return kConsentRequestErrorInvalidAppId; + else + return kConsentRequestErrorInvalidOperation; + } + LogWarning("GMA: Unknown RequestError returned by UMP Android SDK: %d (%s)", + (int)error, message ? message : ""); + return kConsentRequestErrorUnknown; +} + +ConsentFormError +ConsentInfoInternalAndroid::CppConsentFormErrorFromAndroidFormError( + jint error, const char* message) { + if (error == enums().formerror_success) return kConsentFormSuccess; + if (error == enums().formerror_internal) return kConsentFormErrorInternal; + if (error == enums().formerror_timeout) return kConsentFormErrorTimeout; + if (error == enums().formerror_invalid_operation) { + // Error strings taken directly from the UMP Android SDK. + if (message && strcasestr(message, "no available form") != nullptr) + return kConsentFormErrorUnavailable; + else if (message && strcasestr(message, "form is not required") != nullptr) + return kConsentFormErrorUnavailable; + else if (message && + strcasestr(message, "can only be invoked once") != nullptr) + return kConsentFormErrorAlreadyUsed; + else + return kConsentFormErrorInvalidOperation; + } + LogWarning("GMA: Unknown RequestError returned by UMP Android SDK: %d (%s)", + (int)error, message ? message : ""); + return kConsentFormErrorUnknown; +} + +Future ConsentInfoInternalAndroid::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + if (RequestConsentInfoUpdateLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentRequestErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); + JNIEnv* env = GetJNIEnv(); + + jlong future_handle = static_cast(handle.get().id()); + jboolean tag_for_under_age_of_consent = + params.tag_for_under_age_of_consent ? JNI_TRUE : JNI_FALSE; + jint debug_geography = AndroidDebugGeographyFromCppDebugGeography( + params.debug_settings.debug_geography); + jobject debug_device_ids_list = + util::StdVectorToJavaList(env, params.debug_settings.debug_device_ids); + env->CallVoidMethod(helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kRequestConsentInfoUpdate), + future_handle, tag_for_under_age_of_consent, + debug_geography, debug_device_ids_list); + + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentRequestErrorInternal, + exception_message.c_str()); + } else { + has_requested_consent_info_update_ = true; + } + env->DeleteLocalRef(debug_device_ids_list); + + return MakeFuture(futures(), handle); +} + +ConsentStatus ConsentInfoInternalAndroid::GetConsentStatus() { + if (!valid()) { + return kConsentStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jint result = env->CallIntMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kGetConsentStatus)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return kConsentStatusUnknown; + } + return CppConsentStatusFromAndroid(result); +} + +ConsentFormStatus ConsentInfoInternalAndroid::GetConsentFormStatus() { + if (!valid() || !has_requested_consent_info_update_) { + return kConsentFormStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jboolean is_available = env->CallBooleanMethod( + helper_, consent_info_helper::GetMethodId( + consent_info_helper::kIsConsentFormAvailable)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return kConsentFormStatusUnknown; + } + return (is_available == JNI_FALSE) ? kConsentFormStatusUnavailable + : kConsentFormStatusAvailable; +} + +Future ConsentInfoInternalAndroid::LoadConsentForm() { + if (LoadConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kLoadConsentForm), + future_handle); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalAndroid::ShowConsentForm(FormParent parent) { + if (ShowConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); + JNIEnv* env = GetJNIEnv(); + + jlong future_handle = static_cast(handle.get().id()); + jboolean success = env->CallBooleanMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kShowConsentForm), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } else if (success == JNI_FALSE) { + CompleteFuture( + handle, kConsentFormErrorUnavailable, + "The consent form is unavailable. Please call LoadConsentForm and " + "ensure it completes successfully before calling ShowConsentForm."); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalAndroid::LoadAndShowConsentFormIfRequired( + FormParent parent) { + if (LoadAndShowConsentFormIfRequiredLastResult().status() == + kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); + + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod( + helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kLoadAndShowConsentFormIfRequired), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + + return MakeFuture(futures(), handle); +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalAndroid::GetPrivacyOptionsRequirementStatus() { + if (!valid()) { + return kPrivacyOptionsRequirementStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jint result = env->CallIntMethod( + helper_, consent_info_helper::GetMethodId( + consent_info_helper::kGetPrivacyOptionsRequirementStatus)); + return CppPrivacyOptionsRequirementStatusFromAndroid(result); +} + +Future ConsentInfoInternalAndroid::ShowPrivacyOptionsForm( + FormParent parent) { + if (ShowPrivacyOptionsFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); + + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod(helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kShowPrivacyOptionsForm), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + + return MakeFuture(futures(), handle); +} + +bool ConsentInfoInternalAndroid::CanRequestAds() { + JNIEnv* env = GetJNIEnv(); + jboolean can_request = env->CallBooleanMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kCanRequestAds)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return false; + } + return (can_request == JNI_FALSE) ? false : true; +} + +void ConsentInfoInternalAndroid::Reset() { + JNIEnv* env = GetJNIEnv(); + env->CallVoidMethod( + helper_, consent_info_helper::GetMethodId(consent_info_helper::kReset)); + util::CheckAndClearJniExceptions(env); +} + +JNIEnv* ConsentInfoInternalAndroid::GetJNIEnv() { + return firebase::util::GetThreadsafeJNIEnv(java_vm_); +} +jobject ConsentInfoInternalAndroid::activity() { return activity_; } + +void ConsentInfoInternalAndroid::CompleteFutureFromJniCallback( + JNIEnv* env, jint future_fn, FutureHandleId handle_id, int java_error_code, + const char* error_message) { + if (!futures()->ValidFuture(handle_id)) { + // This future is no longer valid, so no need to complete it. + return; + } + if (future_fn < 0 || future_fn >= enums().function_count) { + // Called with an invalid function ID, ignore this callback. + return; + } + FutureHandle raw_handle(handle_id); + SafeFutureHandle handle(raw_handle); + if (future_fn == enums().function_request_consent_info_update) { + // RequestConsentInfoUpdate uses the ConsentRequestError enum. + ConsentRequestError error_code = CppConsentRequestErrorFromAndroidFormError( + java_error_code, error_message); + CompleteFuture(handle, error_code, error_message); + } else { + // All other methods use the ConsentFormError enum. + ConsentFormError error_code = + CppConsentFormErrorFromAndroidFormError(java_error_code, error_message); + CompleteFuture(handle, error_code, error_message); + } +} + +} // namespace internal +} // namespace ump +} // namespace gma +} // namespace firebase diff --git a/gma/src/android/ump/consent_info_internal_android.h b/gma/src/android/ump/consent_info_internal_android.h new file mode 100644 index 0000000000..4c0498671f --- /dev/null +++ b/gma/src/android/ump/consent_info_internal_android.h @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Google LLC + * + * 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_GMA_SRC_ANDROID_UMP_CONSENT_INFO_INTERNAL_ANDROID_H_ +#define FIREBASE_GMA_SRC_ANDROID_UMP_CONSENT_INFO_INTERNAL_ANDROID_H_ + +#include + +#include "app/src/util_android.h" +#include "firebase/internal/mutex.h" +#include "gma/src/common/ump/consent_info_internal.h" + +namespace firebase { +namespace gma { +namespace ump { +namespace internal { + +class ConsentInfoInternalAndroid : public ConsentInfoInternal { + public: + ConsentInfoInternalAndroid(JNIEnv* env, jobject activity); + ~ConsentInfoInternalAndroid() override; + + ConsentStatus GetConsentStatus() override; + ConsentFormStatus GetConsentFormStatus() override; + + Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) override; + Future LoadConsentForm() override; + Future ShowConsentForm(FormParent parent) override; + + Future LoadAndShowConsentFormIfRequired(FormParent parent) override; + + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus() override; + Future ShowPrivacyOptionsForm(FormParent parent) override; + + bool CanRequestAds() override; + + void Reset() override; + + bool valid() { return (helper_ != nullptr); } + + JNIEnv* GetJNIEnv(); + jobject activity(); + + private: + struct EnumCache { + jint consentstatus_unknown; + jint consentstatus_required; + jint consentstatus_not_required; + jint consentstatus_obtained; + + jint formerror_success; + jint formerror_internal; + jint formerror_network; + jint formerror_invalid_operation; + jint formerror_timeout; + + jint debug_geography_disabled; + jint debug_geography_eea; + jint debug_geography_not_eea; + + jint privacy_options_requirement_unknown; + jint privacy_options_requirement_required; + jint privacy_options_requirement_not_required; + + jint function_request_consent_info_update; + jint function_load_consent_form; + jint function_show_consent_form; + jint function_load_and_show_consent_form_if_required; + jint function_show_privacy_options_form; + jint function_count; + }; + + // JNI native method callback for ConsentInfoHelper.completeFuture. + // Calls CompleteFutureFromJniCallback() below. + static void JNI_ConsentInfoHelper_completeFuture( + JNIEnv* env, jclass clazz, jint future_fn, + jlong consent_info_internal_ptr, jlong future_handle, jint error_code, + jobject error_message_obj); + + // Complete the given Future when called from JNI. + void CompleteFutureFromJniCallback(JNIEnv* env, jint future_fn, + FutureHandleId handle_id, int error_code, + const char* error_message); + + // Cache Java enum field values in the struct below. + void CacheEnumValues(JNIEnv* env); + + // Enum conversion methods. + ConsentStatus CppConsentStatusFromAndroid(jint status); + PrivacyOptionsRequirementStatus CppPrivacyOptionsRequirementStatusFromAndroid( + jint status); + jint AndroidDebugGeographyFromCppDebugGeography(ConsentDebugGeography geo); + ConsentRequestError CppConsentRequestErrorFromAndroidFormError( + jint error, const char* message = nullptr); + ConsentFormError CppConsentFormErrorFromAndroidFormError( + jint error, const char* message = nullptr); + + const EnumCache& enums() { return enums_; } + + static ConsentInfoInternalAndroid* s_instance; + static firebase::Mutex s_instance_mutex; + + EnumCache enums_; + + JavaVM* java_vm_; + jobject activity_; + jobject helper_; + + // Needed for GetConsentFormStatus to return Unknown. + bool has_requested_consent_info_update_; +}; + +} // namespace internal +} // namespace ump +} // namespace gma +} // namespace firebase + +#endif // FIREBASE_GMA_SRC_ANDROID_UMP_CONSENT_INFO_INTERNAL_ANDROID_H_ diff --git a/gma/src/common/ump/consent_info.cc b/gma/src/common/ump/consent_info.cc index 89f2ed0410..334a11d735 100644 --- a/gma/src/common/ump/consent_info.cc +++ b/gma/src/common/ump/consent_info.cc @@ -56,13 +56,12 @@ ConsentInfo* ConsentInfo::GetInstance(::firebase::InitResult* init_result_out) { ConsentInfo* consent_info = new ConsentInfo(); #if FIREBASE_PLATFORM_ANDROID - InitResult result = - consent_info->Initialize(/* jni_env, activity */); // TODO(b/291622888) + InitResult result = consent_info->Initialize(jni_env, activity); #else InitResult result = consent_info->Initialize(); #endif + if (init_result_out) *init_result_out = result; if (result != kInitResultSuccess) { - if (init_result_out) *init_result_out = result; delete consent_info; return nullptr; } @@ -85,11 +84,19 @@ ConsentInfo::~ConsentInfo() { s_instance_ = nullptr; } +#if FIREBASE_PLATFORM_ANDROID +InitResult ConsentInfo::Initialize(JNIEnv* jni_env, jobject activity) { + FIREBASE_ASSERT(!internal_); + internal_ = internal::ConsentInfoInternal::CreateInstance(jni_env, activity); + return internal_ ? kInitResultSuccess : kInitResultFailedMissingDependency; +} +#else InitResult ConsentInfo::Initialize() { FIREBASE_ASSERT(!internal_); internal_ = internal::ConsentInfoInternal::CreateInstance(); return kInitResultSuccess; } +#endif // Below this, everything is a passthrough to ConsentInfoInternal. If there is // no internal_ pointer (e.g. it's been cleaned up), return default values and diff --git a/gma/src/common/ump/consent_info_internal.h b/gma/src/common/ump/consent_info_internal.h index 88160a341c..94d87a5ec0 100644 --- a/gma/src/common/ump/consent_info_internal.h +++ b/gma/src/common/ump/consent_info_internal.h @@ -22,6 +22,11 @@ #include "firebase/future.h" #include "firebase/gma/ump.h" #include "firebase/gma/ump/types.h" +#include "firebase/internal/platform.h" + +#if FIREBASE_PLATFORM_ANDROID +#include +#endif namespace firebase { namespace gma { @@ -44,7 +49,11 @@ class ConsentInfoInternal { // Implemented in platform-specific code to instantiate a // platform-specific subclass. +#if FIREBASE_PLATFORM_ANDROID + static ConsentInfoInternal* CreateInstance(JNIEnv* jni_env, jobject activity); +#else static ConsentInfoInternal* CreateInstance(); +#endif virtual ConsentStatus GetConsentStatus() = 0; virtual ConsentFormStatus GetConsentFormStatus() = 0; diff --git a/gma/src/include/firebase/gma/ump/consent_info.h b/gma/src/include/firebase/gma/ump/consent_info.h index 32abf089f8..c092f6f8f1 100644 --- a/gma/src/include/firebase/gma/ump/consent_info.h +++ b/gma/src/include/firebase/gma/ump/consent_info.h @@ -202,8 +202,7 @@ class ConsentInfo { private: ConsentInfo(); #if FIREBASE_PLATFORM_ANDROID - // TODO(b/291622888) Implement Android-specific Initialize.. - InitResult Initialize(/* JNIEnv* jni_env, jobject activity */); + InitResult Initialize(JNIEnv* jni_env, jobject activity); #else InitResult Initialize(); #endif // FIREBASE_PLATFORM_ANDROID diff --git a/gma/src/ios/ump/consent_info_internal_ios.mm b/gma/src/ios/ump/consent_info_internal_ios.mm index 08d2a0d004..266f554328 100644 --- a/gma/src/ios/ump/consent_info_internal_ios.mm +++ b/gma/src/ios/ump/consent_info_internal_ios.mm @@ -85,6 +85,14 @@ static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { Future ConsentInfoInternalIos::RequestConsentInfoUpdate( const ConsentRequestParameters& params) { + if (RequestConsentInfoUpdateLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentRequestErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); @@ -162,6 +170,14 @@ static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { } Future ConsentInfoInternalIos::LoadConsentForm() { + if (LoadConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); loaded_form_ = nil; @@ -191,6 +207,14 @@ static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { } Future ConsentInfoInternalIos::ShowConsentForm(FormParent parent) { + if (ShowConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); if (!loaded_form_) { @@ -219,6 +243,15 @@ static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { Future ConsentInfoInternalIos::LoadAndShowConsentFormIfRequired( FormParent parent) { + if (LoadAndShowConsentFormIfRequiredLastResult().status() == + kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); @@ -260,6 +293,14 @@ static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { } Future ConsentInfoInternalIos::ShowPrivacyOptionsForm(FormParent parent) { + if (ShowPrivacyOptionsFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); util::DispatchAsyncSafeMainQueue(^{ diff --git a/gma/src_java/com/google/firebase/gma/internal/cpp/ConsentInfoHelper.java b/gma/src_java/com/google/firebase/gma/internal/cpp/ConsentInfoHelper.java new file mode 100644 index 0000000000..4b2bf1852b --- /dev/null +++ b/gma/src_java/com/google/firebase/gma/internal/cpp/ConsentInfoHelper.java @@ -0,0 +1,312 @@ +/* + * Copyright 2023 Google LLC + * + * 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.gma.internal.cpp; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import com.google.android.ump.ConsentDebugSettings; +import com.google.android.ump.ConsentForm; +import com.google.android.ump.ConsentForm.OnConsentFormDismissedListener; +import com.google.android.ump.ConsentInformation; +import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateFailureListener; +import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateSuccessListener; +import com.google.android.ump.ConsentInformation.PrivacyOptionsRequirementStatus; +import com.google.android.ump.ConsentRequestParameters; +import com.google.android.ump.FormError; +import com.google.android.ump.UserMessagingPlatform; +import com.google.android.ump.UserMessagingPlatform.OnConsentFormLoadFailureListener; +import com.google.android.ump.UserMessagingPlatform.OnConsentFormLoadSuccessListener; +import java.util.ArrayList; + +/** + * Helper class to make interactions between the GMA UMP C++ wrapper and the Android UMP API. + */ +public class ConsentInfoHelper { + // C++ nullptr for use with the callbacks. + private static final long CPP_NULLPTR = 0; + + // Synchronization object for thread safe access to: + private final Object mLock = new Object(); + // Pointer to the internal ConsentInfoInternalAndroid C++ object. + // This can be reset back to 0 by calling disconnect(). + private long mInternalPtr = 0; + // The Activity that this was initialized with. + private Activity mActivity = null; + // The loaded consent form, if any. + private ConsentForm mConsentForm = null; + + // Create our own local passthrough version of these enum object values + // as integers, to make it easier for the C++ SDK to access them. + public static final int PRIVACY_OPTIONS_REQUIREMENT_UNKNOWN = + PrivacyOptionsRequirementStatus.UNKNOWN.ordinal(); + public static final int PRIVACY_OPTIONS_REQUIREMENT_REQUIRED = + PrivacyOptionsRequirementStatus.REQUIRED.ordinal(); + public static final int PRIVACY_OPTIONS_REQUIREMENT_NOT_REQUIRED = + PrivacyOptionsRequirementStatus.NOT_REQUIRED.ordinal(); + + // Enum values for tracking which function we are calling back. + // Ensure these are incremental starting at 0. + // These don't have to match ConsentInfoFn, as the C++ code will + // use these Java enums directly. + public static final int FUNCTION_REQUEST_CONSENT_INFO_UPDATE = 0; + public static final int FUNCTION_LOAD_CONSENT_FORM = 1; + public static final int FUNCTION_SHOW_CONSENT_FORM = 2; + public static final int FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED = 3; + public static final int FUNCTION_SHOW_PRIVACY_OPTIONS_FORM = 4; + public static final int FUNCTION_COUNT = 5; + + public ConsentInfoHelper(long consentInfoInternalPtr, Activity activity) { + synchronized (mLock) { + mInternalPtr = consentInfoInternalPtr; + mActivity = activity; + // Test the callbacks and fail quickly if something's wrong. + completeFuture(-1, CPP_NULLPTR, CPP_NULLPTR, 0, null); + } + } + + public int getConsentStatus() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.getConsentStatus(); + } + + public void requestConsentInfoUpdate(final long futureHandle, boolean tagForUnderAgeOfConsent, + int debugGeography, ArrayList debugIdList) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_REQUEST_CONSENT_INFO_UPDATE; + + ConsentDebugSettings.Builder debugSettingsBuilder = null; + + // Only create and use debugSettingsBuilder if a debug option is set. + if (debugGeography != ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED) { + debugSettingsBuilder = + new ConsentDebugSettings.Builder(mActivity).setDebugGeography(debugGeography); + } + if (debugIdList != null && debugIdList.size() > 0) { + if (debugSettingsBuilder == null) { + debugSettingsBuilder = new ConsentDebugSettings.Builder(mActivity); + } + for (int i = 0; i < debugIdList.size(); i++) { + debugSettingsBuilder = debugSettingsBuilder.addTestDeviceHashedId(debugIdList.get(i)); + } + } + ConsentRequestParameters.Builder paramsBuilder = + new ConsentRequestParameters.Builder().setTagForUnderAgeOfConsent(tagForUnderAgeOfConsent); + + if (debugSettingsBuilder != null) { + paramsBuilder = paramsBuilder.setConsentDebugSettings(debugSettingsBuilder.build()); + } + + final ConsentRequestParameters params = paramsBuilder.build(); + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + consentInfo.requestConsentInfoUpdate(mActivity, params, + new OnConsentInfoUpdateSuccessListener() { + @Override + public void onConsentInfoUpdateSuccess() { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } + } + }, + new OnConsentInfoUpdateFailureListener() { + @Override + public void onConsentInfoUpdateFailure(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + }); + } + }); + } + + public void loadConsentForm(final long futureHandle) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_LOAD_CONSENT_FORM; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.loadConsentForm(mActivity, + new OnConsentFormLoadSuccessListener() { + @Override + public void onConsentFormLoadSuccess(ConsentForm form) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + mConsentForm = form; + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } + } + }, + new OnConsentFormLoadFailureListener() { + @Override + public void onConsentFormLoadFailure(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + mConsentForm = null; + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + }); + } + }); + } + + public boolean showConsentForm(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return false; + } + final int functionId = FUNCTION_SHOW_CONSENT_FORM; + ConsentForm consentForm; + synchronized (mLock) { + if (mConsentForm == null) { + // Consent form was not loaded, return an error. + return false; + } + consentForm = mConsentForm; + mConsentForm = null; + } + final ConsentForm consentFormForThread = consentForm; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + consentFormForThread.show(activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + // Consent form is loaded. + return true; + } + + public void loadAndShowConsentFormIfRequired(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + } + + public int getPrivacyOptionsRequirementStatus() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.getPrivacyOptionsRequirementStatus().ordinal(); + } + + public void showPrivacyOptionsForm(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_SHOW_PRIVACY_OPTIONS_FORM; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.showPrivacyOptionsForm( + activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + } + + public boolean canRequestAds() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.canRequestAds(); + } + + public boolean isConsentFormAvailable() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.isConsentFormAvailable(); + } + + public void reset() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + consentInfo.reset(); + } + + /** Disconnect the helper from the native object. */ + public void disconnect() { + synchronized (mLock) { + mInternalPtr = CPP_NULLPTR; + } + } + public static native void completeFuture( + int futureFn, long nativeInternalPtr, long futureHandle, int errorCode, String errorMessage); +} diff --git a/messaging/messaging_java/build.gradle b/messaging/messaging_java/build.gradle index 9a2126f779..6b2fccefc9 100644 --- a/messaging/messaging_java/build.gradle +++ b/messaging/messaging_java/build.gradle @@ -81,6 +81,14 @@ afterEvaluate { 'https://github.com/google/flatbuffers.git', flatbuffersDir } + exec { + executable 'git' + args 'apply', + '../../scripts/git/patches/flatbuffers/0001-remove-unused-var.patch', + '--verbose', + '--directory', + 'messaging/messaging_java/build/flatbuffers' + } } // Locate or build flatc. diff --git a/release_build_files/Android/firebase_dependencies.gradle b/release_build_files/Android/firebase_dependencies.gradle index 8d6fa09c28..cd5244449f 100644 --- a/release_build_files/Android/firebase_dependencies.gradle +++ b/release_build_files/Android/firebase_dependencies.gradle @@ -27,7 +27,8 @@ def firebaseDependenciesMap = [ 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links'], 'firestore' : ['com.google.firebase:firebase-firestore'], 'functions' : ['com.google.firebase:firebase-functions'], - 'gma' : ['com.google.android.gms:play-services-ads:22.3.0'], + 'gma' : ['com.google.android.gms:play-services-ads:22.3.0', + 'com.google.android.ump:user-messaging-platform:2.1.0'], 'installations' : ['com.google.firebase:firebase-installations'], 'invites' : ['com.google.firebase:firebase-invites'], // Messaging has an additional local dependency to include. diff --git a/testing/test_framework/src/android/android_firebase_test_framework.cc b/testing/test_framework/src/android/android_firebase_test_framework.cc index 03513c0924..8c37eec1a1 100644 --- a/testing/test_framework/src/android/android_firebase_test_framework.cc +++ b/testing/test_framework/src/android/android_firebase_test_framework.cc @@ -268,8 +268,56 @@ int FirebaseTest::GetGooglePlayServicesVersion() { } std::string FirebaseTest::GetDebugDeviceId() { - // TODO(jsimantov): Add this for Android. - return "placeholder-device-id"; + static char* device_id = nullptr; + if (!device_id) { + JNIEnv* env = app_framework::GetJniEnv(); + jobject activity = app_framework::GetActivity(); + jclass test_helper_class = app_framework::FindClass( + env, activity, "com/google/firebase/example/TestHelper"); + if (env->ExceptionCheck()) { + LogError("Couldn't find TestHelper class"); + env->ExceptionDescribe(); + env->ExceptionClear(); + return ""; + } + jmethodID get_id = + env->GetStaticMethodID(test_helper_class, "getDebugDeviceId", + "(Landroid/content/Context;)Ljava/lang/String;"); + + if (env->ExceptionCheck()) { + LogError("Couldn't look up getDebugDeviceId method"); + env->ExceptionDescribe(); + env->ExceptionClear(); + return ""; + } + jobject device_id_obj = + env->CallStaticObjectMethod(test_helper_class, get_id, activity); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return ""; + } + jstring device_id_jstring = static_cast(device_id_obj); + const char* device_id_text = + env->GetStringUTFChars(device_id_jstring, nullptr); + + if (env->ExceptionCheck()) { + LogError("Couldn't get debug device ID"); + env->ExceptionDescribe(); + env->ExceptionClear(); + return ""; + } + device_id = new char[strlen(device_id_text) + 1]; + strcpy(device_id, device_id_text); // NOLINT + + env->ReleaseStringUTFChars(device_id_jstring, device_id_text); + env->DeleteLocalRef(device_id_jstring); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } + return device_id; } } // namespace firebase_test_framework diff --git a/testing/test_framework/src/android/java/com/google/firebase/example/TestHelper.java b/testing/test_framework/src/android/java/com/google/firebase/example/TestHelper.java index f2fb23d5ea..36151b6b65 100644 --- a/testing/test_framework/src/android/java/com/google/firebase/example/TestHelper.java +++ b/testing/test_framework/src/android/java/com/google/firebase/example/TestHelper.java @@ -14,12 +14,18 @@ package com.google.firebase.example; +import android.content.ContentResolver; import android.content.Context; import android.os.Build; +import android.provider.Settings; import android.util.Log; import java.lang.Class; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; /** * A simple class with test helper methods. @@ -58,4 +64,28 @@ public static int getGooglePlayServicesVersion(Context context) { } return 0; } + + private static String md5(String message) { + String result = null; + if (message == null || message.length() == 0) + return ""; + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(message.getBytes(), 0, message.length()); + result = String.format("%032X", new BigInteger(1, md5.digest())); + } catch (NoSuchAlgorithmException ex) { + result = ""; + } catch (ArithmeticException ex) { + return ""; + } + return result; + } + + public static String getDebugDeviceId(Context context) { + ContentResolver contentResolver = context.getContentResolver(); + if (contentResolver == null) { + return "error"; + } + return md5(Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)); + } }