Add Calorie AI Android app

This commit is contained in:
Daniel 2026-05-17 17:43:16 +02:00
commit 5cdc1c1142
10 changed files with 728 additions and 0 deletions

View file

@ -0,0 +1,51 @@
name: Android CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk
GRADLE_VERSION: 8.10.2
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update
apt-get install -y --no-install-recommends ca-certificates curl unzip openjdk-17-jdk
- name: Install Android SDK
run: |
mkdir -p "$ANDROID_HOME/cmdline-tools"
curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/android-tools.zip
unzip -q /tmp/android-tools.zip -d /tmp/android-tools
mv /tmp/android-tools/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
"platform-tools" \
"platforms;android-35" \
"build-tools;35.0.0"
- name: Install Gradle
run: |
curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o /tmp/gradle.zip
unzip -q /tmp/gradle.zip -d /opt
ln -s "/opt/gradle-${GRADLE_VERSION}/bin/gradle" /usr/local/bin/gradle
- name: Build debug APK
run: gradle --no-daemon :app:assembleDebug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: calorie-ai-debug-apk
path: app/build/outputs/apk/debug/app-debug.apk

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.gradle/
build/
app/build/
local.properties
*.iml
.idea/

37
README.md Normal file
View file

@ -0,0 +1,37 @@
# Calorie AI Android
Native Android calorie tracker with optional meal images and admin-configurable AI models.
## Features
- Add meals by description, portion estimate, and optional image.
- Analyze meals through OpenAI-compatible chat completions.
- Uses a vision model for image interpretation when an image is attached.
- Uses a tasking model to estimate calories, macros, and notes.
- Stores daily meal entries locally on device.
- Admin settings control API base URL, API key, vision model, and tasking model. The two model fields can contain the same model name.
## Build
Open this folder in Android Studio and run the `app` configuration, or build with Gradle if available:
```bash
gradle :app:assembleDebug
```
Forgejo Actions builds the debug APK on every push to `main` and uploads it as the `calorie-ai-debug-apk` artifact.
## AI Endpoint
The app expects an OpenAI-compatible endpoint at:
```text
{API base URL}/chat/completions
```
Example base URLs:
- `https://api.openai.com/v1`
- An emulator host-loopback URL ending in `:11434/v1` for a local OpenAI-compatible service
Default admin PIN is `admin`. Change it in the Admin AI Settings panel after first launch.

16
app/build.gradle Normal file
View file

@ -0,0 +1,16 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.danvics.calorieai'
compileSdk 35
defaultConfig {
applicationId 'com.danvics.calorieai'
minSdk 26
targetSdk 35
versionCode 1
versionName '1.0'
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="Calorie AI"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,569 @@
package com.danvics.calorieai;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.text.InputType;
import android.util.Base64;
import android.view.Gravity;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.database.Cursor;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class MainActivity extends Activity {
private static final int PICK_IMAGE_REQUEST = 11;
private SharedPreferences prefs;
private LinearLayout root;
private EditText descriptionInput;
private EditText measureInput;
private TextView selectedImageText;
private TextView statusText;
private Button analyzeButton;
private Uri selectedImageUri;
private String selectedImageDataUri;
private String selectedImageName;
private boolean adminUnlocked;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
prefs = getSharedPreferences("calorie_ai", MODE_PRIVATE);
adminUnlocked = prefs.getBoolean("admin_unlocked", false);
buildScreen();
}
private void buildScreen() {
ScrollView scrollView = new ScrollView(this);
scrollView.setBackgroundColor(Color.rgb(248, 244, 236));
root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setPadding(dp(18), dp(24), dp(18), dp(24));
scrollView.addView(root);
TextView title = text("Calorie AI", 30, true);
title.setTextColor(Color.rgb(38, 55, 45));
root.addView(title);
TextView subtitle = text("Track what you eat each day. Add a description, portion estimate, and optional image for AI calorie analysis.", 15, false);
subtitle.setTextColor(Color.rgb(91, 99, 92));
subtitle.setPadding(0, dp(6), 0, dp(18));
root.addView(subtitle);
addTodaySummary();
addMealForm();
addEntries();
addAdminSettings();
setContentView(scrollView);
}
private void addTodaySummary() {
JSONArray todayEntries = entriesForDate(todayKey());
int calories = 0;
int protein = 0;
int carbs = 0;
int fat = 0;
for (int i = 0; i < todayEntries.length(); i++) {
JSONObject entry = todayEntries.optJSONObject(i);
if (entry == null) continue;
calories += entry.optInt("calories");
protein += entry.optInt("proteinGrams");
carbs += entry.optInt("carbsGrams");
fat += entry.optInt("fatGrams");
}
LinearLayout card = card();
TextView heading = text("Today", 18, true);
card.addView(heading);
card.addView(text(todayKey() + " | " + todayEntries.length() + " meals", 13, false));
TextView totals = text(calories + " kcal", 34, true);
totals.setTextColor(Color.rgb(47, 125, 89));
totals.setPadding(0, dp(8), 0, dp(2));
card.addView(totals);
card.addView(text("Protein " + protein + "g | Carbs " + carbs + "g | Fat " + fat + "g", 14, false));
root.addView(card);
}
private void addMealForm() {
LinearLayout card = card();
card.addView(text("Add Meal", 20, true));
descriptionInput = input("What did you eat? Example: chicken rice bowl with avocado");
descriptionInput.setMinLines(3);
descriptionInput.setGravity(Gravity.TOP);
card.addView(descriptionInput);
measureInput = input("Portion or measure. Example: 450g, 1 large bowl, 2 slices");
card.addView(measureInput);
Button imageButton = button("Choose optional image");
imageButton.setOnClickListener(v -> pickImage());
card.addView(imageButton);
selectedImageText = text("No image selected", 13, false);
selectedImageText.setTextColor(Color.rgb(91, 99, 92));
selectedImageText.setPadding(0, dp(6), 0, dp(6));
card.addView(selectedImageText);
analyzeButton = button("Analyze and save meal");
analyzeButton.setOnClickListener(v -> analyzeMeal());
card.addView(analyzeButton);
statusText = text("", 13, false);
statusText.setPadding(0, dp(8), 0, 0);
card.addView(statusText);
root.addView(card);
}
private void addEntries() {
LinearLayout card = card();
card.addView(text("Today's Meals", 20, true));
JSONArray todayEntries = entriesForDate(todayKey());
if (todayEntries.length() == 0) {
TextView empty = text("No meals logged today.", 14, false);
empty.setTextColor(Color.rgb(91, 99, 92));
card.addView(empty);
root.addView(card);
return;
}
for (int i = todayEntries.length() - 1; i >= 0; i--) {
JSONObject entry = todayEntries.optJSONObject(i);
if (entry == null) continue;
TextView meal = text(entry.optString("time") + " " + entry.optString("mealName", "Meal"), 16, true);
meal.setPadding(0, dp(12), 0, dp(2));
card.addView(meal);
card.addView(text(entry.optInt("calories") + " kcal | P " + entry.optInt("proteinGrams")
+ "g | C " + entry.optInt("carbsGrams") + "g | F " + entry.optInt("fatGrams") + "g", 14, false));
card.addView(text(entry.optString("description"), 13, false));
String notes = entry.optString("notes");
if (!notes.isEmpty()) {
TextView note = text(notes, 13, false);
note.setTextColor(Color.rgb(91, 99, 92));
card.addView(note);
}
}
root.addView(card);
}
private void addAdminSettings() {
LinearLayout card = card();
card.addView(text("Admin AI Settings", 20, true));
if (!adminUnlocked) {
EditText pin = input("Admin PIN");
pin.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
card.addView(pin);
Button unlock = button("Unlock settings");
unlock.setOnClickListener(v -> {
String expected = prefs.getString("admin_pin", "admin");
if (expected.equals(pin.getText().toString())) {
adminUnlocked = true;
prefs.edit().putBoolean("admin_unlocked", true).apply();
buildScreen();
} else {
pin.setError("Incorrect PIN");
}
});
card.addView(unlock);
card.addView(text("Default PIN: admin", 12, false));
root.addView(card);
return;
}
EditText baseUrl = input("API base URL, e.g. https://api.openai.com/v1");
baseUrl.setText(prefs.getString("api_base_url", ""));
card.addView(baseUrl);
EditText apiKey = input("API key. Leave blank for local endpoints that do not need auth");
apiKey.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
apiKey.setText(prefs.getString("api_key", ""));
card.addView(apiKey);
EditText visionModel = input("Vision model");
visionModel.setText(prefs.getString("vision_model", "gpt-4o-mini"));
card.addView(visionModel);
EditText taskModel = input("Tasking model");
taskModel.setText(prefs.getString("task_model", "gpt-4o-mini"));
card.addView(taskModel);
EditText newPin = input("New admin PIN, optional");
newPin.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
card.addView(newPin);
Button save = button("Save AI settings");
save.setOnClickListener(v -> {
SharedPreferences.Editor editor = prefs.edit()
.putString("api_base_url", baseUrl.getText().toString().trim())
.putString("api_key", apiKey.getText().toString().trim())
.putString("vision_model", visionModel.getText().toString().trim())
.putString("task_model", taskModel.getText().toString().trim());
String pin = newPin.getText().toString().trim();
if (!pin.isEmpty()) editor.putString("admin_pin", pin);
editor.apply();
save.setText("Saved");
});
card.addView(save);
Button lock = button("Lock admin settings");
lock.setOnClickListener(v -> {
adminUnlocked = false;
prefs.edit().putBoolean("admin_unlocked", false).apply();
buildScreen();
});
card.addView(lock);
TextView help = text("The vision model reads attached meal photos. The tasking model estimates calories, macros, and notes from the full meal context. Both fields may use the same model.", 12, false);
help.setTextColor(Color.rgb(91, 99, 92));
card.addView(help);
root.addView(card);
}
private void pickImage() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(intent, PICK_IMAGE_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != PICK_IMAGE_REQUEST || resultCode != RESULT_OK || data == null) return;
selectedImageUri = data.getData();
if (selectedImageUri == null) return;
try {
getContentResolver().takePersistableUriPermission(selectedImageUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException ignored) {
// Some document providers grant one-shot read access only; analysis still works in-session.
}
selectedImageName = displayName(selectedImageUri);
selectedImageDataUri = null;
selectedImageText.setText("Selected: " + selectedImageName);
}
private void analyzeMeal() {
String description = descriptionInput.getText().toString().trim();
String measure = measureInput.getText().toString().trim();
String baseUrl = prefs.getString("api_base_url", "").trim();
String taskModel = prefs.getString("task_model", "").trim();
if (description.isEmpty() && selectedImageUri == null) {
status("Describe the meal or attach an image.", true);
return;
}
if (baseUrl.isEmpty() || taskModel.isEmpty()) {
status("Admin must set API base URL and tasking model first.", true);
return;
}
analyzeButton.setEnabled(false);
status("Analyzing meal...", false);
new Thread(() -> {
try {
String vision = "";
if (selectedImageUri != null) {
if (selectedImageDataUri == null) selectedImageDataUri = readImageAsDataUri(selectedImageUri);
String visionModel = prefs.getString("vision_model", "").trim();
if (!visionModel.isEmpty()) {
vision = requestVisionDescription(baseUrl, visionModel, description, selectedImageDataUri);
}
}
JSONObject estimate = requestTaskEstimate(baseUrl, taskModel, description, measure, vision);
saveEntry(description, measure, vision, estimate, selectedImageUri != null);
runOnUiThread(() -> {
selectedImageUri = null;
selectedImageDataUri = null;
selectedImageName = null;
buildScreen();
});
} catch (Exception e) {
runOnUiThread(() -> {
analyzeButton.setEnabled(true);
status("AI analysis failed: " + e.getMessage(), true);
});
}
}).start();
}
private String requestVisionDescription(String baseUrl, String model, String description, String imageDataUri) throws Exception {
JSONArray content = new JSONArray();
content.put(new JSONObject()
.put("type", "text")
.put("text", "Describe this food photo for calorie estimation. Include likely foods, portion sizes, visible quantities, sauces, and uncertainty. User description: " + description));
content.put(new JSONObject()
.put("type", "image_url")
.put("image_url", new JSONObject().put("url", imageDataUri)));
JSONArray messages = new JSONArray();
messages.put(new JSONObject().put("role", "user").put("content", content));
JSONObject body = new JSONObject()
.put("model", model)
.put("messages", messages)
.put("temperature", 0.2);
return chatCompletion(baseUrl, body);
}
private JSONObject requestTaskEstimate(String baseUrl, String model, String description, String measure, String vision) throws Exception {
String prompt = "You are a nutrition logging assistant. Estimate calories and macros for one meal. "
+ "Return only JSON with keys mealName, calories, proteinGrams, carbsGrams, fatGrams, notes. "
+ "Use integer grams and calories. Mention uncertainty in notes.\n\n"
+ "User description: " + description + "\n"
+ "User measure: " + measure + "\n"
+ "Vision description: " + vision;
JSONArray messages = new JSONArray();
messages.put(new JSONObject().put("role", "system").put("content", "Return strict JSON only."));
messages.put(new JSONObject().put("role", "user").put("content", prompt));
JSONObject body = new JSONObject()
.put("model", model)
.put("messages", messages)
.put("temperature", 0.1);
String content = chatCompletion(baseUrl, body);
return parseEstimate(content);
}
private String chatCompletion(String baseUrl, JSONObject body) throws Exception {
String urlString = baseUrl.replaceAll("/+$", "") + "/chat/completions";
HttpURLConnection connection = (HttpURLConnection) new URL(urlString).openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(20000);
connection.setReadTimeout(60000);
connection.setRequestProperty("Content-Type", "application/json");
String apiKey = prefs.getString("api_key", "").trim();
if (!apiKey.isEmpty()) connection.setRequestProperty("Authorization", "Bearer " + apiKey);
connection.setDoOutput(true);
try (OutputStream output = connection.getOutputStream()) {
output.write(body.toString().getBytes(StandardCharsets.UTF_8));
}
int code = connection.getResponseCode();
InputStream stream = code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream();
String response = readText(stream);
if (code < 200 || code >= 300) throw new IllegalStateException("HTTP " + code + ": " + response);
JSONObject json = new JSONObject(response);
return json.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content")
.trim();
}
private JSONObject parseEstimate(String content) throws JSONException {
String cleaned = content.trim();
if (cleaned.startsWith("```")) {
cleaned = cleaned.replaceFirst("^```json", "").replaceFirst("^```", "");
cleaned = cleaned.replaceFirst("```$", "").trim();
}
int start = cleaned.indexOf('{');
int end = cleaned.lastIndexOf('}');
if (start >= 0 && end > start) cleaned = cleaned.substring(start, end + 1);
JSONObject estimate = new JSONObject(cleaned);
return new JSONObject()
.put("mealName", estimate.optString("mealName", "Meal"))
.put("calories", Math.max(0, estimate.optInt("calories")))
.put("proteinGrams", Math.max(0, estimate.optInt("proteinGrams")))
.put("carbsGrams", Math.max(0, estimate.optInt("carbsGrams")))
.put("fatGrams", Math.max(0, estimate.optInt("fatGrams")))
.put("notes", estimate.optString("notes", ""))
.put("raw", content);
}
private void saveEntry(String description, String measure, String vision, JSONObject estimate, boolean hasImage) throws JSONException {
JSONArray entries = allEntries();
JSONObject entry = new JSONObject()
.put("date", todayKey())
.put("time", LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm")))
.put("description", description)
.put("measure", measure)
.put("imageIncluded", hasImage)
.put("visionDescription", vision)
.put("mealName", estimate.optString("mealName", "Meal"))
.put("calories", estimate.optInt("calories"))
.put("proteinGrams", estimate.optInt("proteinGrams"))
.put("carbsGrams", estimate.optInt("carbsGrams"))
.put("fatGrams", estimate.optInt("fatGrams"))
.put("notes", estimate.optString("notes"))
.put("rawAi", estimate.optString("raw"));
entries.put(entry);
prefs.edit().putString("entries", entries.toString()).apply();
}
private JSONArray allEntries() {
try {
return new JSONArray(prefs.getString("entries", "[]"));
} catch (JSONException e) {
return new JSONArray();
}
}
private JSONArray entriesForDate(String date) {
JSONArray filtered = new JSONArray();
JSONArray entries = allEntries();
for (int i = 0; i < entries.length(); i++) {
JSONObject entry = entries.optJSONObject(i);
if (entry != null && date.equals(entry.optString("date"))) filtered.put(entry);
}
return filtered;
}
private String readImageAsDataUri(Uri uri) throws Exception {
try (InputStream input = getContentResolver().openInputStream(uri)) {
if (input == null) throw new IllegalStateException("Could not read selected image");
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192];
int read;
while ((read = input.read(data)) != -1) buffer.write(data, 0, read);
String type = getContentResolver().getType(uri);
if (type == null) type = "image/jpeg";
return "data:" + type + ";base64," + Base64.encodeToString(buffer.toByteArray(), Base64.NO_WRAP);
}
}
private String displayName(Uri uri) {
try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index >= 0) return cursor.getString(index);
}
} catch (Exception ignored) {
}
return uri.getLastPathSegment() == null ? "selected image" : uri.getLastPathSegment();
}
private String readText(InputStream input) throws Exception {
if (input == null) return "";
StringBuilder builder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) builder.append(line);
}
return builder.toString();
}
private String todayKey() {
return LocalDate.now().toString();
}
private void status(String message, boolean error) {
statusText.setText(message);
statusText.setTextColor(error ? Color.rgb(164, 52, 52) : Color.rgb(47, 125, 89));
}
private LinearLayout card() {
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(dp(16), dp(16), dp(16), dp(16));
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, 0, dp(14));
layout.setLayoutParams(params);
GradientDrawable background = new GradientDrawable();
background.setColor(Color.WHITE);
background.setCornerRadius(dp(18));
background.setStroke(dp(1), Color.rgb(229, 221, 207));
layout.setBackground(background);
return layout;
}
private TextView text(String value, int sp, boolean bold) {
TextView view = new TextView(this);
view.setText(value);
view.setTextSize(sp);
view.setTextColor(Color.rgb(38, 55, 45));
view.setLineSpacing(0, 1.12f);
if (bold) view.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
return view;
}
private EditText input(String hint) {
EditText editText = new EditText(this);
editText.setHint(hint);
editText.setTextSize(14);
editText.setSingleLine(false);
editText.setPadding(dp(12), dp(10), dp(12), dp(10));
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, dp(10), 0, 0);
editText.setLayoutParams(params);
GradientDrawable background = new GradientDrawable();
background.setColor(Color.rgb(252, 250, 246));
background.setCornerRadius(dp(12));
background.setStroke(dp(1), Color.rgb(219, 211, 197));
editText.setBackground(background);
return editText;
}
private Button button(String label) {
Button button = new Button(this);
button.setText(label);
button.setTextColor(Color.WHITE);
button.setAllCaps(false);
GradientDrawable background = new GradientDrawable();
background.setColor(Color.rgb(47, 125, 89));
background.setCornerRadius(dp(12));
button.setBackground(background);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, dp(10), 0, 0);
button.setLayoutParams(params);
return button;
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">#F8F4EC</item>
<item name="android:navigationBarColor">#F8F4EC</item>
<item name="android:colorAccent">#2F7D59</item>
</style>
</resources>

3
build.gradle Normal file
View file

@ -0,0 +1,3 @@
plugins {
id 'com.android.application' version '8.7.3' apply false
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

18
settings.gradle Normal file
View file

@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = 'Calorie AI'
include ':app'