Skip to content

feat(android): add JSON share intent for AI-assisted meal import - #587

Open
Marc4k wants to merge 2 commits into
simonoppowa:developfrom
Marc4k:feature/share-intent-json
Open

feat(android): add JSON share intent for AI-assisted meal import#587
Marc4k wants to merge 2 commits into
simonoppowa:developfrom
Marc4k:feature/share-intent-json

Conversation

@Marc4k

@Marc4k Marc4k commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Adds Android share intent support so any app — including AI assistants
using the bundled food-tracker skill — can share meal JSON directly to
OpenNutriTracker. The skill accepts natural-language descriptions and
food photos; it generates the app's existing JSON export format and the
user taps Share → OpenNutriTracker, confirms the import dialog, and the
meals land in their diary.

iOS share intent is out of scope for this PR (no Share Extension target
exists yet). The Flutter service returns null on iOS so there is no
regression — tracked as a follow-up.

Ai Skill

food-tracker.skill.zip

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / tooling
  • Localization
  • Other

Related issues

Changes

  • AndroidAndroidManifest.xml registers ACTION_SEND intent-filters
    for text/plain and application/json so the app appears in the system
    share sheet
  • AndroidMainActivity.kt captures the shared text via a
    MethodChannel and holds it until Flutter reads it (handles both cold-start
    and singleTop onNewIntent)
  • FlutterShareIntentService bridges the channel; returns null on
    iOS (no-op fallback)
  • FlutterMainScreen checks for a pending share on cold start and on
    every foreground resume via AppLifecycleListener, shows a confirmation
    dialog, and imports approved meals using the existing
    ImportMealsJsonUsecase
  • l10n — two new strings (shareJsonImportContent,
    shareJsonImportErrorLabel) added to all 9 supported locales (EN + DE, CS,
    IT, PL, SK, TR, UK, ZH)
  • fix — pre-existing curly_braces_in_flow_control_structures lint in
    meal_relevance_ranker.dart that blocked CI

Screenshots / recordings

example.mp4

Test plan

  • Described steps below were followed locally
  • Unit / widget tests added or updated (if applicable)
  • Manual check on Android
  • Manual check on iOS (if applicable) — N/A, iOS not yet supported

Steps

  1. Build and install the app (fvm flutter run --flavor develop)
  2. Use the food-tracker AI skill to generate a meal — it outputs JSON in
    ONT's export format
  3. In the AI app, tap Share → OpenNutriTracker
  4. Confirm the import dialog — meals appear in today's diary
  5. Share invalid/non-JSON text to ONT — error snackbar is shown, no crash

Checklist

  • Code follows project style (just format / 120-char line width)
  • New interactive widgets include Semantics(identifier: '...') where
    needed (share-json-import-confirm on the dialog confirm button)
  • Localization updated in ARBs and lib/generated/ when user-facing
    strings change
  • Codegen ran if DBOs/DTOs/env changed (just build) — no codegen changes
  • No secrets or .env values committed
  • PR title follows conventional commit style (e.g. feat:, fix:, chore:)

@Marc4k Marc4k changed the title Feature/share intent json feat(android): add JSON share intent for AI-assisted meal import Jul 25, 2026
@Marc4k

Marc4k commented Jul 31, 2026

Copy link
Copy Markdown
Author

@simonoppowa, any updates on this one? I use it daily and would love to switch back to the main app instead of running my fork. Happy to also implement iOS support if that's a blocker for merging.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Android JSON share-intent support for importing AI-generated meals through the existing import pipeline.

Changes:

  • Adds Android share-intent capture and Flutter lifecycle handling.
  • Adds localized confirmation and error messages.
  • Fixes an unrelated Dart lint violation.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
android/app/src/main/AndroidManifest.xml Registers supported share MIME types.
android/app/src/main/kotlin/.../MainActivity.kt Captures shared payloads through a method channel.
lib/core/presentation/main_screen.dart Confirms, imports, and refreshes shared meals.
lib/core/utils/share_intent_service.dart Provides the Flutter method-channel bridge.
lib/features/add_meal/util/meal_relevance_ranker.dart Adds required flow-control braces.
lib/l10n/intl_en.arb Adds English strings.
lib/l10n/intl_de.arb Adds German strings.
lib/l10n/intl_cs.arb Adds Czech strings.
lib/l10n/intl_it.arb Adds Italian strings.
lib/l10n/intl_pl.arb Adds Polish strings.
lib/l10n/intl_sk.arb Adds Slovak strings.
lib/l10n/intl_tr.arb Adds Turkish strings.
lib/l10n/intl_uk.arb Adds Ukrainian strings.
lib/l10n/intl_zh.arb Adds Chinese strings.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +14 to +18
try {
return await _channel.invokeMethod<String?>('getSharedText');
} on PlatformException {
return null;
}

if (parsed.intakes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(S.of(context).shareJsonImportErrorLabel)),
Comment on lines +101 to +102
final result = await locator<ImportMealsJsonUsecase>().importFromJsonString(text);
if (!mounted) return;
if (intent?.action != Intent.ACTION_SEND) return null
val type = intent.type ?: return null
if (!type.startsWith("text/") && type != "application/json") return null
return intent.getStringExtra(Intent.EXTRA_TEXT)
Comment on lines +108 to +110
final message = result.imported > 0
? S.of(context).csvImportSuccessLabel(result.imported)
: S.of(context).shareJsonImportErrorLabel;

@simonoppowa simonoppowa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @Marc4k — and apologies for the wait. The delay was not about your PR. It sat behind the fork-workflow approval gate, so CI had never run on it at all; that was mine to notice and I didn't. I've approved it now and everything passes: analyzer, unit tests, intl check, and both platform builds. (One iOS integration run hung at 41 minutes and I re-ran it — flaky simulator, unrelated to your changes; the re-run finished in 16m54s.)

Reading the diff, this is more careful than most: launchMode="singleTop" really is already set so onNewIntent fires as your comment says, consume-and-clear on the native side genuinely does prevent a double import, mounted is checked after every await, the lifecycle listener is disposed, and the new string is translated into all nine locales with no parity drift. That is good work.


The open question is ours, not yours

JsonMealImporter assigns MealSourceEntity.custom, so macros an assistant estimated land in the diary indistinguishable from a meal the user typed in themselves — and flow from there into TDEE, goal progress and trends.

That is the same question #250 was closed over. The closing comment there explicitly pre-empted the bring-your-own-model route: it doesn't change the underlying claim, which is a model estimated this rather than this came from a database. Your design sidesteps the cost and privacy objections in that thread completely — there's no key, no network call, no dependency — but not the provenance one.

That is a project decision we owe you an answer on before this PR can be judged fairly, and it is being worked through on #599. Nothing in your code causes it and no change you could make here would settle it. I did not want to leave you guessing about what the actual blocker is.


Code review

Ordered by what I'd want addressed, if and when the above is resolved.

1. text/plain puts the app in every text share sheet. AndroidManifest.xml — with a text/plain filter, any text share on the device (a browser URL, a selected paragraph) now offers OpenNutriTracker, and choosing it launches to a "Could not read shared JSON" snackbar. That footprint reaches every user, including those who never use the skill. application/json alone is much narrower; the trade-off is that plenty of apps only share as text/plain. Worth a deliberate decision either way.

2. onNewIntent can silently discard a pending share. MainActivity.kt:

override fun onNewIntent(intent: Intent) { pendingSharedText = extractSharedText(intent) }

A non-SEND intent arriving before Flutter reads the value — a notification tap, for instance — overwrites it with null and the share is lost with no error. Suggest only assigning when the extract is non-null. It also never calls setIntent(intent); harmless today because you use the parameter, but it leaves getIntent() stale for anything added later.

3. The dialog's count can exceed what actually imports. main_screen.dart shows parsed.intakes.length, then imports and reports result.imported. If the usecase drops entries, the user approved 5 and silently received 3. The JSON is also parsed twice — once to count, once to import.

4. No tests for the new plumbing. The importer itself is well covered (json_meal_importer_test.dart, import_meals_json_usecase_test.dart), but ShareIntentService and _handleSharedMeal have none. A widget test with a faked MethodChannel would cover confirm / cancel / malformed cheaply, and this path writes to the diary.

5. Unrelated change. The meal_relevance_ranker.dart hunk is a line wrapped in braces, presumably from dart format. Harmless, but it is noise in the diff.


One to think about rather than fix

The dialog says "These meals will be added to your diary" with a count, and never shows the food names or the numbers. That is consistent with the existing QR importer, so it is not something you introduced — but that path imports another user's database-sourced export, while this imports model estimates, and the same dialog carries a different amount of trust in the two cases.

For contrast, the multi-item review screen being built on #599 states in its own doc comment that its confirmation exists "so nothing reaches the diary that the user has not looked at". A count is not looking at it.


Not reviewed

I have not opened the linked food-tracker.skill.zip. It is a meaningful part of what this PR enables, so what I have reviewed is the transport rather than what travels over it.

Heads-up on overlap

There is a multi-item logging feature in flight on feature/ai-assisted-meal-logging (#599) that also touches meal_relevance_ranker.dart and the same intake-writing path. Neither branch knows about the other yet. Worth coordinating before either merges so we don't collide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants