The SDK actually executes several steps before sending the request to the aforementioned API, which include barcode scanning, and OCR for nutrition facts recognition
If the SDK did not detect a barcode or nutrition facts in the image, it will proceed with the preparation of the request
The body of the request will contain a base64 encoded string of the native image
The SDK adds the Localization-ISO header to the request if the locale code was set using the function updateLanguage
The successful response is mapped to a list of PassioAdvisorFoodInfo objects, using PassioAdvisorFoodInfo mapping
The response contains the same JSON as the Food searchresponse, but here the alternatives field is not parsed, and the response will only contain one result in the JSON array
The SDK will add the encoded metadata into the url of the request. The metadata contains a boolean key called shortName, used in the PassioFoodDataInfo
private const val SHORT_NAME = "shortName"
fun encodeMetadata(shortName: Boolean): String {
val metadata = """{"$SHORT_NAME": $shortName}"""
return Base64.encodeToString(metadata.toByteArray(), Base64.NO_WRAP)
}
The SDK adds the Localization-ISO header to the request if the locale code was set using the function updateLanguage
Holds macro nutrient information for the default serving size of a database reference object
Mapping function to a PassioSearchNutritionPreview object, where the NutritionPreviewResult is the root JSON object:
internal fun NutritionPreviewResult.toDataModel(): PassioSearchNutritionPreview {
val servingUnit = portion.name ?: ""
val servingQuantity = if (portion.suggestedQuantity != null) {
portion.suggestedQuantity!!.first()
} else {
portion.quantity ?: 0.0
}
val weightUnit = portion.weight?.unit ?: ""
val weightQuantity = (portion.weight?.value ?: 0.0) * servingQuantity
return PassioSearchNutritionPreview(
calories.roundToInt(),
carbs,
protein,
fat,
fiber,
servingUnit,
servingQuantity,
weightUnit,
weightQuantity
)
}
PassioFoodDataInfo
SDK representation of the database reference object
Does not contain all of the nutritional data, but has reference fields that can be used to fetch the full data
Mapping function to a PassioFoodDataInfo object from a list of JSON objects, where the response.results represents an array of JSON objects:
val searchResults = response.results.map {
PassioFoodDataInfo(
it.refCode,
it.displayName,
it.brandName,
it.iconId,
it.score,
it.scoredName,
it.labelId,
it.type,
it.resultId,
false, // depends on the API
it.nutritionPreview.toDataModel(),
it.tags
)
}
PassioFoodItem
Contains all of the nutritional information for a single database entry
Top-level object that contains one or more ingredients
Parsing from search routes
fun fromSearchResponse(item: ResponseFoodItem, useShortName: Boolean): PassioFoodItem {
val id = item.internalId
val refCode = item.refCode ?: ""
val name = if (useShortName) {
item.internalName.ifEmpty { item.displayName }
} else {
item.displayName.ifEmpty { item.internalName }
}
val details = if (useShortName) {
item.displayName.ifEmpty {
if (item.ingredients.size == 1) item.ingredients.first().branded?.owner
?: "" else ""
}
} else {
if (item.ingredients.size == 1) item.ingredients.first().branded?.owner?.ifEmpty { item.displayName }
?: "" else ""
}
val iconId = item.iconId
val ingredients = item.ingredients.map { PassioIngredient.fromResponse(it, iconId) }
.toMutableList()
val amount = if (item.ingredients.size == 1) {
PassioFoodAmount.fromResponse(item.ingredients.first().portions)
} else {
val ingredientsWeight =
ingredients.map { it.weight() }.reduce { acc, unitMass -> acc + unitMass }
.gramsValue()
PassioFoodAmount.fromResponse(item.portions, ingredientsWeight)
}
}
Parsing from packaged product routes
Used in the SDK when fetching from a productCode, usually during barcode scanning
fun fromUPCResponse(item: ResponseIngredient): PassioFoodItem {
val id = item.id ?: ""
val refCode = item.refCode ?: ""
val name = item.name ?: ""
val details = item.branded?.owner ?: ""
val iconId = item.iconId ?: ""
val ingredient = PassioIngredient.fromResponse(item, iconId)
val amount = PassioFoodAmount.fromResponse(item.portions)
return PassioFoodItem(id, refCode, name, details, iconId, amount, listOf(ingredient))
}
PassioIngredient
Contains nutritional and serving size data for a single food item
fun fromResponse(result: ResponseIngredient, topLevelIcon: String): PassioIngredient {
val refCode = result.refCode ?: ""
val nutrients = PassioNutrients(UnitMass(Grams, 100.0), result.nutrients ?: listOf())
val amount = PassioFoodAmount.fromResponse(result.portions)
val metadata = PassioFoodMetadata.fromResponse(result)
val iconId = result.iconId ?: topLevelIcon
return PassioIngredient(
result.id ?: "",
refCode,
result.name ?: "",
iconId,
amount,
nutrients,
metadata
)
}
PassioFoodAmount
SDK representation of serving portions, including serving quantities, units and their weights
If there are no portions in the JSON response, the SDK manually creates a 100 grams serving size
The SDK offers conversion between units cup, teaspoon and tablespoon. If the JSON response contains only one or two of these units, the SDK will calculate the remaning ones and add them to the list of serving sizes
Mapping function that creates a PassioFoodAmount, using a ResponsePortions that represents a top level JSON object:
internal fun fromResponse(
portions: List<ResponsePortions>?,
ingredientWeight: Double? = null
): PassioFoodAmount {
if (portions == null) {
// If no portions are found, return 100 grams as the default
return PassioFoodAmount(
listOf(PassioServingSize(100.0, Grams.unitName)),
listOf(PassioServingUnit(Grams.unitName, UnitMass(Grams, 1.0)))
)
}
val servingSizes = mutableListOf<PassioServingSize>()
val servingUnits = mutableListOf<PassioServingUnit>()
portions.forEach { portion ->
val servingSizesAndUnit = getServingSizeAndUnit(portion) ?: return@forEach
servingSizes.addAll(servingSizesAndUnit.first)
servingUnits.add(servingSizesAndUnit.second)
}
if (servingSizes.isEmpty() && ingredientWeight != null) {
// If there are no serving sizes for a recipe, add "1 serving" with the weight of
// the summed up weight of the ingredients
servingUnits.add(
PassioServingUnit(
SERVING_UNIT_NAME,
UnitMass(Grams, ingredientWeight)
)
)
servingSizes.add(PassioServingSize(1.0, SERVING_UNIT_NAME))
}
val conversionUnits = ServingUnitHelper.getConversionUnits(servingUnits)
if (conversionUnits.isNotEmpty()) {
servingUnits.addAll(conversionUnits)
}
// Grams should always be present in the serving unit
if (servingUnits.find { it.unitName == Grams.unitName } == null) {
servingUnits.add(PassioServingUnit(Grams.unitName, UnitMass(Grams, 1.0)))
}
// 100 grams should always be present in the serving sizes
if (servingSizes.find { it.unitName == Grams.unitName && it.quantity == 100.0 } == null) {
servingSizes.add(PassioServingSize(100.0, Grams.unitName))
}
return PassioFoodAmount(servingSizes, servingUnits)
}
private fun getServingSizeAndUnit(
portion: ResponsePortions
): Pair<List<PassioServingSize>, PassioServingUnit>? {
val servings = mutableListOf<PassioServingSize>()
val unit: PassioServingUnit
if (portion.name == null) {
return null
}
if (portion.suggestedQuantity != null) {
val suggestedServings = portion.suggestedQuantity!!.map { suggestedQuantity ->
PassioServingSize(suggestedQuantity, portion.name!!)
}
servings.addAll(suggestedServings)
} else if (portion.quantity != null) {
servings.add(PassioServingSize(portion.quantity!!, portion.name!!))
} else {
return null
}
if (portion.weight == null) {
return null
}
val unitMass = parseWeight(portion.weight!!) ?: return null
unit = PassioServingUnit(portion.name!!, unitMass)
return Pair(servings, unit)
}
private fun parseWeight(responseWeight: ResponseWeight): UnitMass? {
return when (responseWeight.unit) {
"g", "G", "gr", "GR" -> UnitMass(Grams, responseWeight.value ?: 0.0)
"ml", "ML" -> UnitMass(Milliliters, responseWeight.value ?: 0.0)
"l", "L" -> UnitMass(Liters, responseWeight.value ?: 0.0)
else -> {
PassioLog.e(
this::class.java.simpleName,
"Unknown unit of weight: ${responseWeight.unit}"
)
null
}
}
}
fun getConversionUnits(currentUnits: List<PassioServingUnit>): List<PassioServingUnit> {
val addedUnits = mutableMapOf<String, PassioServingUnit>()
val teaspoonLabels = listOf("tsp", "teaspoon")
val tablespoonLabels = listOf("tbsp", "tablespoon")
val cupLabels = listOf("cup")
currentUnits.forEach { unit ->
when (unit.unitName) {
"cup" -> {
if (currentUnits.find { it.unitName in teaspoonLabels } == null) {
addedUnits["tsp"] = PassioServingUnit("tsp", unit.weight * (1.0/48.0))
}
if (currentUnits.find { it.unitName in tablespoonLabels } == null) {
addedUnits["tbsp"] = PassioServingUnit("tbsp", unit.weight * (1.0/16.0))
}
}
"tbsp", "tablespoon" -> {
if (currentUnits.find { it.unitName in cupLabels } == null) {
addedUnits["cup"] = PassioServingUnit("cup", unit.weight * 16.0)
}
if (currentUnits.find { it.unitName in teaspoonLabels } == null) {
addedUnits["tsp"] = PassioServingUnit("tsp", unit.weight * (1.0/3.0))
}
}
"tsp", "teaspoon" -> {
if (currentUnits.find { it.unitName in cupLabels } == null) {
addedUnits["cup"] = PassioServingUnit("cup", unit.weight * 48.0)
}
if (currentUnits.find { it.unitName in tablespoonLabels } == null) {
addedUnits["tbsp"] = PassioServingUnit("tbsp", unit.weight * 3.0)
}
}
}
}
return addedUnits.keys.map { addedUnits[it]!! }
}
PassioNutrients
Data class that contains all of the macro and micro nutrients supported by the Passio Nutritional database
The SDK parses the values for Calories and VitaminA differently from other nutrients. Calories are parsed as UnitEnergy, and VitaminA has a Double value
The nutrientDefaults serves as a helper list that references the nutrient field, it's JSON "id" and JSON "shortName"
Mapping function that creates a PassioNutrients object, using a list of ResponseNutrient objects that represent a top level JSON array
Contains miscellaneous data about the food ingredient like it's data source, product code, tags and ingredient description
Mapping function that creates a PassioFoodMetadata, using a ResponseIngredient that represents a top level JSON object:
fun fromResponse(result: ResponseIngredient?): PassioFoodMetadata {
val metadata = PassioFoodMetadata()
if (result == null) {
return metadata
}
if (result.origin != null) {
val originsTemp = mutableListOf<PassioFoodOrigin>()
result.origin!!.forEach {
originsTemp.add(PassioFoodOrigin(it.id, it.source, result.licenseCopy))
}
metadata.foodOrigins = originsTemp
}
metadata.barcode = result.branded?.productCode
metadata.ingredientsDescription = result.branded?.ingredients
metadata.tags = result.tags
return metadata
}
PassioAdvisorFoodInfo
Represents a object returned by Passio's LLM services
Mapping function that creates a list of PassioAdvisorFoodInfo objects, using a json String as a top level JSON object:
private fun parseIngredients(json: String): List<PassioAdvisorFoodInfo> {
val jArray = JSONArray(json)
val list = mutableListOf<PassioAdvisorFoodInfo>()
for (i in 0 until jArray.length()) {
val it = ResponseAdvisorItem(jArray.getJSONObject(i).toString())
val foodDataInfo = PassioFoodDataInfo(
it.refCode,
it.shortName.ifEmpty { it.displayName },
it.brandName,
it.iconId,
it.score,
it.scoredName,
it.labelId,
it.type,
it.resultId,
true,
it.nutritionPreview.toDataModel(),
it.tags
)
val model = PassioAdvisorFoodInfo(
it.ingredientName,
it.portionSize,
it.weightGrams,
foodDataInfo,
null,
null,
PassioFoodResultType.FOOD_ITEM
)
list.add(model)
}
return list
PassioSpeechRecognitionModel
Represents a result from the speech recognition service
val jArray = JSONArray(result)
val list = mutableListOf<PassioSpeechRecognitionModel>()
for (i in 0 until jArray.length()) {
val voiceItem = ResponseVoice(jArray.getJSONObject(i).toString())
voiceItem.items.forEach {
val foodDataInfo = PassioFoodDataInfo(
it.refCode,
it.shortName.ifEmpty { it.displayName },
it.brandName,
it.iconId,
it.score,
it.scoredName,
it.labelId,
it.type,
it.resultId,
true,
it.nutritionPreview.toDataModel(),
it.tags
)
val dataInfo = PassioAdvisorFoodInfo(
it.ingredientName,
it.portionSize,
it.weightGrams,
foodDataInfo,
null,
null,
PassioFoodResultType.FOOD_ITEM,
)
val model = PassioSpeechRecognitionModel(
voiceItem.action.toLogAction(),
voiceItem.mealTime.toMealTime(),
voiceItem.date,
dataInfo
)
list.add(model)
}
}
internal fun String.toLogAction(): PassioLogAction {
return when (this.lowercase()) {
"add" -> PassioLogAction.ADD
"remove" -> PassioLogAction.REMOVE
else -> PassioLogAction.NONE
}
}
internal fun String.toMealTime(): PassioMealTime? {
return when (this.lowercase()) {
"breakfast" -> PassioMealTime.BREAKFAST
"lunch" -> PassioMealTime.LUNCH
"dinner" -> PassioMealTime.DINNER
"snack" -> PassioMealTime.SNACK
else -> null
}
}
Typically used in the SDK when fetching from a reference object
Mapping function that creates a PassioFoodItem, using a ResponseFoodItem that represents a top level JSON object, and parsing functions for and:
Mapping function that creates a PassioFoodItem, using a ResponseFoodItem that represents a top level JSON object, and parsing functions for and:
Mapping function that creates a PassioIngredient, using a ResponseIngredient that represents a top level JSON object, and parsing functions for , and :
If the contains more then one ingredient, and there are no portions in the JSON response, the SDK will manually create a serving size called "Serving" , with it's weight being the summed up weight of all of the ingredients
Mapping function that creates a list of PassioSpeechRecognitionModel objects, using a result String as a top level JSON array, also usign and mapping: