diff --git a/analytics/integration_test/src/integration_test.cc b/analytics/integration_test/src/integration_test.cc index 575ff30b82..bedccee516 100644 --- a/analytics/integration_test/src/integration_test.cc +++ b/analytics/integration_test/src/integration_test.cc @@ -271,6 +271,32 @@ TEST_F(FirebaseAnalyticsTest, TestLogEventWithMultipleParameters) { sizeof(kLevelUpParameters) / sizeof(kLevelUpParameters[0])); } +TEST_F(FirebaseAnalyticsTest, TestLogEventWithComplexParameters) { + // Define the items that will go into the kParameterItems list. + firebase::Variant first_item = firebase::Variant::EmptyMap(); + first_item.map()[firebase::analytics::kParameterItemID] = "SKU_12345"; + first_item.map()[firebase::analytics::kParameterItemName] = "Horse Armor DLC"; + firebase::Variant second_item = firebase::Variant::EmptyMap(); + second_item.map()[firebase::analytics::kParameterItemID] = "SKU_67890"; + second_item.map()[firebase::analytics::kParameterItemName] = + "Gold Horse Armor DLC"; + + // Define the parameters that are sent with the ViewCart event. + const firebase::analytics::Parameter kViewCartParameters[] = { + firebase::analytics::Parameter(firebase::analytics::kParameterCurrency, + "USD"), + firebase::analytics::Parameter(firebase::analytics::kParameterValue, + 30.03), + firebase::analytics::Parameter( + firebase::analytics::kParameterItems, + std::vector{first_item, second_item}), + }; + + firebase::analytics::LogEvent( + firebase::analytics::kEventViewCart, kViewCartParameters, + sizeof(kViewCartParameters) / sizeof(kViewCartParameters[0])); +} + TEST_F(FirebaseAnalyticsTest, TestSetConsent) { // On Android, this test must be performed at the end, after all the tests for // session ID and instance ID. This is because once you call SetConsent to diff --git a/analytics/src/analytics_android.cc b/analytics/src/analytics_android.cc index 16f55b8995..d1279c6ba7 100644 --- a/analytics/src/analytics_android.cc +++ b/analytics/src/analytics_android.cc @@ -355,6 +355,109 @@ void AddToBundle(JNIEnv* env, jobject bundle, const char* key, int64_t value) { env->DeleteLocalRef(key_string); } +// Add an ArrayList to the given Bundle. +void AddArrayListToBundle(JNIEnv* env, jobject bundle, const char* key, + jobject arraylist) { + jstring key_string = env->NewStringUTF(key); + env->CallVoidMethod( + bundle, util::bundle::GetMethodId(util::bundle::kPutParcelableArrayList), + key_string, arraylist); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(key_string); +} + +// Add a Bundle to the given Bundle. +void AddBundleToBundle(JNIEnv* env, jobject bundle, const char* key, + jobject inner_bundle) { + jstring key_string = env->NewStringUTF(key); + env->CallVoidMethod(bundle, + util::bundle::GetMethodId(util::bundle::kPutBundle), + key_string, inner_bundle); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(key_string); +} + +// Declared here so that it can be used, defined below. +jobject MapToBundle(JNIEnv* env, const std::map& map); + +// Converts the given vector into a Java ArrayList. It is up to the +// caller to delete the local reference when done. +jobject VectorOfMapsToArrayList(JNIEnv* env, + const std::vector& vector) { + jobject arraylist = env->NewObject( + util::array_list::GetClass(), + util::array_list::GetMethodId(util::array_list::kConstructor)); + + for (const Variant& element : vector) { + if (element.is_map()) { + jobject bundle = MapToBundle(env, element.map()); + env->CallBooleanMethod( + arraylist, util::array_list::GetMethodId(util::array_list::kAdd), + bundle); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(bundle); + } else { + LogError("VectorOfMapsToArrayList: Unsupported type (%s) within vector.", + Variant::TypeName(element.type())); + } + } + return arraylist; +} + +// Converts and adds the Variant to the given Bundle. +bool AddVariantToBundle(JNIEnv* env, jobject bundle, const char* key, + const Variant& value) { + if (value.is_int64()) { + AddToBundle(env, bundle, key, value.int64_value()); + } else if (value.is_double()) { + AddToBundle(env, bundle, key, value.double_value()); + } else if (value.is_string()) { + AddToBundle(env, bundle, key, value.string_value()); + } else if (value.is_bool()) { + // Just use integer 0 or 1. + AddToBundle(env, bundle, key, + value.bool_value() ? static_cast(1L) + : static_cast(0L)); + } else if (value.is_null()) { + // Just use integer 0 for null. + AddToBundle(env, bundle, key, static_cast(0L)); + } else if (value.is_vector()) { + jobject arraylist = VectorOfMapsToArrayList(env, value.vector()); + AddArrayListToBundle(env, bundle, key, arraylist); + env->DeleteLocalRef(arraylist); + } else if (value.is_map()) { + jobject inner_bundle = MapToBundle(env, value.map()); + AddBundleToBundle(env, bundle, key, inner_bundle); + env->DeleteLocalRef(inner_bundle); + } else { + // A Variant type that couldn't be handled was passed in. + return false; + } + return true; +} + +// Converts the given map into a Java Bundle. It is up to the caller +// to delete the local reference when done. +jobject MapToBundle(JNIEnv* env, const std::map& map) { + jobject bundle = + env->NewObject(util::bundle::GetClass(), + util::bundle::GetMethodId(util::bundle::kConstructor)); + for (const auto& pair : map) { + // Only add elements that use a string key + if (!pair.first.is_string()) { + continue; + } + if (!AddVariantToBundle(env, bundle, pair.first.string_value(), + pair.second)) { + LogError("MapToBundle: Unsupported type (%s) within map with key %s.", + Variant::TypeName(pair.second.type()), + pair.first.string_value()); + } + util::CheckAndClearJniExceptions(env); + } + return bundle; +} + // Log an event with one string parameter. void LogEvent(const char* name, const char* parameter_name, const char* parameter_value) { @@ -404,27 +507,11 @@ void LogEvent(const char* name, const Parameter* parameters, LogEvent(env, name, [env, parameters, number_of_parameters](jobject bundle) { for (size_t i = 0; i < number_of_parameters; ++i) { const Parameter& parameter = parameters[i]; - if (parameter.value.is_int64()) { - AddToBundle(env, bundle, parameter.name, parameter.value.int64_value()); - } else if (parameter.value.is_double()) { - AddToBundle(env, bundle, parameter.name, - parameter.value.double_value()); - } else if (parameter.value.is_string()) { - AddToBundle(env, bundle, parameter.name, - parameter.value.string_value()); - } else if (parameter.value.is_bool()) { - // Just use integer 0 or 1. - AddToBundle(env, bundle, parameter.name, - parameter.value.bool_value() ? static_cast(1L) - : static_cast(0L)); - } else if (parameter.value.is_null()) { - // Just use integer 0 for null. - AddToBundle(env, bundle, parameter.name, static_cast(0L)); - } else { - // Vector or Map were passed in. + if (!AddVariantToBundle(env, bundle, parameter.name, parameter.value)) { + // A Variant type that couldn't be handled was passed in. LogError( "LogEvent(%s): %s is not a valid parameter value type. " - "Container types are not allowed. No event was logged.", + "No event was logged.", parameter.name, Variant::TypeName(parameter.value.type())); } } diff --git a/analytics/src/analytics_ios.mm b/analytics/src/analytics_ios.mm index 4ceb8bdc7a..d56fd61356 100644 --- a/analytics/src/analytics_ios.mm +++ b/analytics/src/analytics_ios.mm @@ -231,6 +231,69 @@ void LogEvent(const char* name) { [FIRAnalytics logEventWithName:@(name) parameters:@{}]; } +// Declared here so that it can be used, defined below. +NSDictionary* MapToDictionary(const std::map& map); + +// Converts the given vector of Maps into an ObjC NSArray of ObjC NSDictionaries. +NSArray* VectorOfMapsToArray(const std::vector& vector) { + NSMutableArray* array = [NSMutableArray arrayWithCapacity:vector.size()]; + for (const Variant& element : vector) { + if (element.is_map()) { + NSDictionary* dict = MapToDictionary(element.map()); + [array addObject:dict]; + } else { + LogError("VectorOfMapsToArray: Unsupported type (%s) within vector.", + Variant::TypeName(element.type())); + } + } + return array; +} + +// Converts and adds the Variant to the given Dictionary. +bool AddVariantToDictionary(NSMutableDictionary* dict, NSString* key, const Variant& value) { + if (value.is_int64()) { + [dict setObject:[NSNumber numberWithLongLong:value.int64_value()] forKey:key]; + } else if (value.is_double()) { + [dict setObject:[NSNumber numberWithDouble:value.double_value()] forKey:key]; + } else if (value.is_string()) { + [dict setObject:SafeString(value.string_value()) forKey:key]; + } else if (value.is_bool()) { + // Just use integer 0 or 1. + [dict setObject:[NSNumber numberWithLongLong:value.bool_value() ? 1 : 0] forKey:key]; + } else if (value.is_null()) { + // Just use integer 0 for null. + [dict setObject:[NSNumber numberWithLongLong:0] forKey:key]; + } else if (value.is_vector()) { + NSArray* array = VectorOfMapsToArray(value.vector()); + [dict setObject:array forKey:key]; + } else if (value.is_map()) { + NSDictionary* inner_dict = MapToDictionary(value.map()); + [dict setObject:inner_dict forKey:key]; + } else { + // A Variant type that couldn't be handled was passed in. + return false; + } + return true; +} + +// Converts the given map into an ObjC dictionary of ObjC objects. +NSDictionary* MapToDictionary(const std::map& map) { + NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:map.size()]; + for (const auto& pair : map) { + // Only add elements that use a string key + if (!pair.first.is_string()) { + continue; + } + NSString* key = SafeString(pair.first.string_value()); + const Variant& value = pair.second; + if (!AddVariantToDictionary(dict, key, value)) { + LogError("MapToDictionary: Unsupported type (%s) within map with key %s.", + Variant::TypeName(value.type()), key); + } + } + return dict; +} + // Log an event with associated parameters. void LogEvent(const char* name, const Parameter* parameters, size_t number_of_parameters) { FIREBASE_ASSERT_RETURN_VOID(internal::IsInitialized()); @@ -239,25 +302,10 @@ void LogEvent(const char* name, const Parameter* parameters, size_t number_of_pa for (size_t i = 0; i < number_of_parameters; ++i) { const Parameter& parameter = parameters[i]; NSString* parameter_name = SafeString(parameter.name); - if (parameter.value.is_int64()) { - [parameters_dict setObject:[NSNumber numberWithLongLong:parameter.value.int64_value()] - forKey:parameter_name]; - } else if (parameter.value.is_double()) { - [parameters_dict setObject:[NSNumber numberWithDouble:parameter.value.double_value()] - forKey:parameter_name]; - } else if (parameter.value.is_string()) { - [parameters_dict setObject:SafeString(parameter.value.string_value()) forKey:parameter_name]; - } else if (parameter.value.is_bool()) { - // Just use integer 0 or 1. - [parameters_dict setObject:[NSNumber numberWithLongLong:parameter.value.bool_value() ? 1 : 0] - forKey:parameter_name]; - } else if (parameter.value.is_null()) { - // Just use integer 0 for null. - [parameters_dict setObject:[NSNumber numberWithLongLong:0] forKey:parameter_name]; - } else { - // Vector or Map were passed in. + if (!AddVariantToDictionary(parameters_dict, parameter_name, parameter.value)) { + // A Variant type that couldn't be handled was passed in. LogError("LogEvent(%s): %s is not a valid parameter value type. " - "Container types are not allowed. No event was logged.", + "No event was logged.", parameter.name, Variant::TypeName(parameter.value.type())); } } diff --git a/app/src/util_android.h b/app/src/util_android.h index 6cfeae5acf..3467c53b22 100644 --- a/app/src/util_android.h +++ b/app/src/util_android.h @@ -491,14 +491,16 @@ METHOD_LOOKUP_DECLARATION(activity, ACTIVITY_METHODS) // Used to setup the cache of Bundle class method IDs to reduce time spent // looking up methods by string. // clang-format off -#define BUNDLE_METHODS(X) \ - X(Constructor, "", "()V"), \ - X(GetString, "getString", "(Ljava/lang/String;)Ljava/lang/String;"), \ - X(KeySet, "keySet", "()Ljava/util/Set;"), \ - X(PutFloat, "putFloat", "(Ljava/lang/String;F)V"), \ - X(PutLong, "putLong", "(Ljava/lang/String;J)V"), \ - X(PutString, "putString", "(Ljava/lang/String;Ljava/lang/String;)V"), \ - X(PutBundle, "putBundle", "(Ljava/lang/String;Landroid/os/Bundle;)V") +#define BUNDLE_METHODS(X) \ + X(Constructor, "", "()V"), \ + X(GetString, "getString", "(Ljava/lang/String;)Ljava/lang/String;"), \ + X(KeySet, "keySet", "()Ljava/util/Set;"), \ + X(PutFloat, "putFloat", "(Ljava/lang/String;F)V"), \ + X(PutLong, "putLong", "(Ljava/lang/String;J)V"), \ + X(PutString, "putString", "(Ljava/lang/String;Ljava/lang/String;)V"), \ + X(PutBundle, "putBundle", "(Ljava/lang/String;Landroid/os/Bundle;)V"), \ + X(PutParcelableArrayList, "putParcelableArrayList", \ + "(Ljava/lang/String;Ljava/util/ArrayList;)V") // clang-format on METHOD_LOOKUP_DECLARATION(bundle, BUNDLE_METHODS) diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 61ad3b5abc..95f1e351e2 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -634,6 +634,8 @@ code. ### Upcoming Release - Changes - General (Android): Reduced minSdkVersion back to 23. + - Analytics: Add support for Parameters of Lists of Dictionaries, needed + by some events such as ViewCart. - Auth (Android): Setting photo_url to empty string with UpdateUserProfile clears the field, making it consistent with the other platforms.