BreakCount Documentation

Technical documentation for the BreakCount Flutter app β€” architecture, services, themes, achievements, personas, widgets, AI scan, live activity, and nearby.

Quick Links

Architecture Overview

Project Structure

lib/
β”œβ”€β”€ app/                    # App-level config
β”‚   β”œβ”€β”€ constants.dart      # Colors, spacing, StorageKeys
β”‚   β”œβ”€β”€ routes.dart         # Named route definitions
β”‚   β”œβ”€β”€ theme.dart          # MaterialApp theme builder
β”‚   β”œβ”€β”€ theme_preset.dart   # ThemePreset model + AppThemeController
β”‚   └── persona_theme_ext.dart  # BuildContext extension for persona tint
β”œβ”€β”€ screens/                # Full-page UI (tabs, settings, review, etc.)
β”œβ”€β”€ widgets/                # Reusable UI components
β”œβ”€β”€ services/               # Business logic, platform channels, storage
β”œβ”€β”€ data/                   # Static data (achievements, personas, school data)
β”œβ”€β”€ models/                 # Data models (Schedule, Subject, Exam, etc.)
β”œβ”€β”€ utils/                  # Helpers (debug_log)
└── main.dart               # Entry point, initialization, BreakCountApp widget

State Management

BreakCount uses a lightweight approach β€” no external state management package:

  • ValueNotifier<T> for reactive state (theme, persona tint, streak)
  • StorageService (SharedPreferences wrapper) for persistence
  • setState() in StatefulWidgets for local UI state
  • Listeners wired in main.dart for cross-cutting concerns

Data Flow

User action β†’ Service method β†’ StorageService.save() β†’ ValueNotifier.value = x
                                                              ↓
                                                    Listeners fire
                                                              ↓
                                              Widget rebuilds / side effects
                                              (e.g., WidgetService.update())

Initialization Order (main.dart)

  1. WidgetsFlutterBinding.ensureInitialized()
  2. Firebase init (Core, Crashlytics, Analytics)
  3. StorageService.init() β€” loads SharedPreferences
  4. AppThemeController.init() β€” restores saved theme
  5. AchievementService.init() β€” loads unlock state
  6. StreakService.init() + recordOpen() β€” daily streak
  7. PersonaService.init() β€” restores active persona
  8. Cross-cutting listeners wired (achievement→widget, persona→widget, theme→widget, streak→achievement)
  9. Notifications, FCM, widget update (fire-and-forget)
  10. runApp(BreakCountApp(...))

Key Patterns

  • Fire-and-forget: Non-critical operations (widget updates, analytics, auto-backup) never block startup.
  • Graceful degradation: Every service wraps platform calls in try/catch; failures are logged, never propagated.
  • Additive architecture: New features (themes, personas, achievements) plug into existing notifier/listener infrastructure without rewrites.

Services Reference

All services live in lib/services/. They are static-method classes (no instantiation needed) unless noted.

Core Services

ServicePurposeKey Methods
StorageServiceSharedPreferences wrapperinit(), getString(), saveBool(), saveString()
SchoolDataServiceFetches/caches school year datafetch(country), getCached(), lastUpdated()
ScheduleServiceCRUD for weekly timetablegetSchedule(), getSubjects(), saveEntry(), clearAll()
ExamServiceCRUD for examsgetExams(), addExam(), deleteExam()
ReminderServiceExam/break remindersgetUpcomingReminders(), addReminder()

Feature Services

ServicePurposeKey Methods
AchievementServiceUnlock tracking, milestone helpersinit(), unlock(id), allUnlocks, onStreakMilestone()
XpServiceXP sum, level, progresstotalXp(), level(), progress(), rankTitle()
QuestServiceDaily quests (3/day)todayQuests(), incrementProgress(), claimReward()
StreakServiceDaily check-in streakinit(), recordOpen(), currentStreak, longestStreak
UnlockServiceTheme/persona gatingisThemeUnlocked(id), isPersonaUnlocked(id), requirements
PersonaServiceActive persona managementinstance.init(), currentNotifier, setPersona(id)
MoodServiceMood tracking + streakscurrentStreak(kind), recordMood()
StudyLogServiceManual study session loglogSession(), weeklyBreakdown(), all()

Platform Services

ServicePurposeKey Methods
WidgetServicePushes data to Android widgetsupdate()
LiveActivityServiceLock-screen countdown controlstart(), stop(), isAvailable(), isRunning()
NotificationServiceLocal notificationsinit(), show(), requestPermissions()
BackupServiceGoogle Drive backup/restorebackup(), restore(), isSignedIn()
ShakeServiceAccelerometer shake detectionstartListening(), onShake callback
MeshServiceNearby Connections P2PstartDiscovery(), sendPayload(), onPeerFound

AI Services

ServicePurposeKey Methods
AiScheduleServiceTimetable photo β†’ entriesparseImage(file, apiKey)
OcrTimetableParserOffline ML Kit pre-passparse(file), parseRecognized(text)
RecapAiServiceWeekly persona recap generationgenerateRecap()

Utility Services

ServicePurpose
AnalyticsServiceFirebase Analytics event logging
CalendarServiceExport exams to device calendar
CalculatorServiceBreak/year calculations
SubjectImportanceServiceCountry-aware subject tagging

Theme System

BreakCount supports 17 theme presets. 6 are always available; 11 unlock via daily streak milestones or achievement triggers.

ThemePreset Model

class ThemePreset {
  final String id;
  final String name;
  final String emoji;
  final bool dark;
  final Color bgDeep;       // Scaffold background
  final Color bgDark;       // Elevated background
  final Color bgSurface;    // Card/surface background
  final Color primary;      // Primary accent
  final Color gradA, gradB; // Decorative gradient
  final Color surfaceBorder;
  final Color? textPrimary; // Override (null = default)
  final double cardOpacity;
}

Default Presets (Always Unlocked)

IDNameEmojiStyle
coffeeCoffeeβ˜•Warm brown on cream
midnightMidnightπŸŒ™Dark with indigo accent
mintMint🌿Fresh green
sakuraSakura🌸Pink blossom
oceanOcean🌊Blue
sunsetSunsetπŸŒ‡Warm orange

Unlockable Presets

IDUnlock Condition
lavender7-day streak
forest14-day streak
aurora30-day streak
paper50-day streak
cosmic75-day streak
neon100-day streak
amoled150-day streak
vapor200-day streak
solarized365-day streak
zenAchievement: all_seasonal_breaks
monoAchievement: achievement_hunter_50

AppThemeController

Global singleton managing the active theme:

  • AppThemeController.notifier β€” ValueNotifier<ThemePreset>, UI listens to this
  • AppThemeController.personaTintNotifier β€” persona accent color overlay
  • AppThemeController.init(savedId) β€” restores from storage
  • AppThemeController.setTheme(preset) β€” switches + persists

Widget Integration

When the theme changes, main.dart's listener calls WidgetService.update(), which writes hex color strings to HomeWidget SharedPreferences. The Android BreakCountWidgetProvider reads these and applies them via RemoteViews.setInt(...).

Adding a New Theme

  1. Add a static const ThemePreset in theme_preset.dart
  2. Add it to the all list
  3. Add an UnlockRequirement entry in unlock_service.dart
  4. The theme picker and widget system pick it up automatically

Achievements & XP System

100+ achievements across 8 categories, with an XP/level system and daily quests.

Achievement Model

class Achievement {
  final String id;
  final String name;
  final String description;
  final IconData icon;
  final AchievementCategory category;
  final AchievementRarity rarity;
  final int xp;              // XP granted on unlock
  final bool secret;         // Hidden until unlocked
}

Categories

schoolProgress, mondayClub, examsSchedule, breakMilestones, powerUser, streaks, themes, personas

Rarities & XP

RarityXPColor
Bronze25Brown
Silver75Grey
Gold200Gold
Platinum500Purple
Secret750Rainbow

XP / Level System

Thresholds: L1=0, L2=200, L3=600, L4=1500, L5=3500, L6=7000, L7=12000, L8=20000, L9=35000, L10=60000

LevelRank Title
1Newcomer
2Rookie
3Survivor
4Veteran
5Master
6Legend
7Mythic
8Ascendant
9Transcendent
10+Eternal

Daily Quests

3 quests per day, deterministically seeded by date + install ID. ~20 quest templates (e.g., open_schedule_tab, tap_countdown_5x, change_persona). Completion grants XP; some high-tier quests unlock themes/personas. Reset at local midnight.

Milestone Helpers

AchievementService exposes helpers that other services call:

  • onStreakMilestone(int days) β€” called by StreakService
  • onThemeUnlocked(String id) β€” called by UnlockService
  • onPersonaUnlocked(String id) β€” called by PersonaService
  • onStudySessionLogged() β€” called by StudyLogService
  • onAchievementCountChanged() β€” self-referential (hunter ladder)

Adding Achievements

  1. Add entry to lib/data/achievements_data.dart with unique id, category, rarity, XP
  2. Add unlock trigger in the appropriate service helper
  3. Run test/achievements_data_test.dart to verify no duplicate IDs

Personas

30 personas, each with a unique emoji, tint color, and full copy palette. The active persona affects app-wide text, colors, confetti, widgets, and weekly recaps.

Persona Model

class Persona {
  final String id;
  final String name;
  final String emoji;
  final Color tint;          // Accent color override
  final bool isDefault;      // Always unlocked
}

Copy System

Copy is keyed by (personaId, slot). Slots include:

  • countdown_far β€” 60+ days
  • countdown_mid β€” 15–60 days
  • countdown_close β€” 2–15 days
  • countdown_imminent β€” 0–2 days
  • break_reveal β€” break just started
  • recap_intro β€” weekly recap opener
  • greeting β€” app open

Base Personas (Always Unlocked)

IDNameEmojiTint
hypeHypeπŸ”₯Orange-red
chillChill😎Blue
dramaticDramatic🎭Purple
sarcasticSarcasticπŸ™ƒTeal

Unlockable Personas

26 additional personas unlock via streak milestones and achievement triggers, registered in UnlockService. Includes: Ghost πŸ‘», Sage πŸ§™, Nerd πŸ€“, Tired πŸ₯±, Ice 🧊, Philosopher 🧐, Volcano πŸŒ‹, Sloth πŸ¦₯, Moon πŸŒ™, Phoenix πŸ¦…, Jester πŸƒ, Hacker πŸ’», Pirate πŸ΄β€β˜ οΈ, Robot πŸ€–, and more.

PersonaService

  • PersonaService.instance β€” singleton
  • currentNotifier β€” ValueNotifier<Persona>, drives UI rebuilds
  • setPersona(id) β€” switches active persona, updates tint notifier, persists
  • init() β€” restores from storage

Persona Tint Integration

AppThemeController.personaTintNotifier is updated whenever the persona changes. The MaterialApp rebuilds with the new tint via persona_theme_ext.dart's BuildContext extension.

Adding a Persona

  1. Add entry to lib/data/personas_data.dart
  2. Add copy entries for all slots in lib/data/persona_copy.dart
  3. Add unlock condition in lib/services/unlock_service.dart
  4. Run test/persona_copy_test.dart to verify all slots are covered

Home Screen Widgets

Four widget sizes (2Γ—1, 2Γ—2, 4Γ—1, 4Γ—2) powered by native Android AppWidgetProvider + the home_widget Flutter package.

Architecture

Flutter (WidgetService.update())
    ↓ writes key-value pairs
HomeWidget SharedPreferences
    ↓ read by
Android BreakCountWidgetProvider (Kotlin)
    ↓ applies to
RemoteViews β†’ AppWidgetManager.updateAppWidget()

Data Payload

KeyTypeDescription
days_until_breakintDays to next break (-1 if none)
next_break_nameStringBreak name
year_progressint0–100
is_on_breakboolCurrently on break
vibe_emojiStringPersona emoji
vibe_copyStringMood-aware one-liner
current_classString?Active class name
theme_bg_hexStringTheme background (#RRGGBB)
theme_primary_hexStringPrimary accent
theme_darkboolDark mode flag
persona_tint_hexStringPersona accent color

Update Triggers

WidgetService.update() is called on app startup, when the theme changes, when the persona changes, when an achievement unlocks, and on background widget interaction.

Layout Files

  • breakcount_widget_2x1.xml β€” horizontal pill
  • breakcount_widget_2x2.xml β€” square card with progress ring
  • breakcount_widget_4x1.xml β€” wide bar
  • breakcount_widget_4x2.xml β€” wide card

AI Timetable Scan & Offline OCR

Offline-first timetable import: ML Kit OCR runs locally first, cloud providers are only called as a fallback.

Flow

User takes photo
    ↓
AiScheduleService.parseImage(file, apiKey)
    ↓
β”Œβ”€ OcrTimetableParser.parse(file) ─── offline pre-pass
β”‚   β”œβ”€ ML Kit TextRecognizer
β”‚   β”œβ”€ Grid detection (rows by Y-coordinate)
β”‚   β”œβ”€ Day column detection (header tokens)
β”‚   β”œβ”€ Subject matching (canonical suggestions)
β”‚   └─ Confidence scoring
β”‚
β”œβ”€ If confidence β‰₯ 0.6 AND entries β‰₯ 20 β†’ return (isOfflineOcr: true)
β”‚
└─ Else fall through to cloud provider:
    β”œβ”€ Groq Llama 4 (if key starts with gsk_)
    └─ Cloudflare Worker proxy (5 free/day, no key needed)
        ↓
AiReviewScreen (edit/delete entries per day)
    ↓
ScheduleService.save()

Offline OCR Algorithm

  1. Run TextRecognizer(script: latin) on the image
  2. Flatten recognized blocks into lines with bounding box coordinates
  3. Group lines into rows by Y-coordinate (tolerance = 0.7Γ— avg line height)
  4. Detect day columns from header row (Mon/Tue/Wed/Thu/Fri in multiple languages)
  5. Walk data rows, match text against country's canonical subject list
  6. Assign each match to nearest day column
  7. Deduplicate by (subject, day, startTime)

Confidence Scoring

confidence = entryScore Γ— 0.6 + daysScore Γ— 0.25 + subjectHitRate Γ— 0.15

entryScore     = entries found / 20 (clamped 0–1)
daysScore      = distinct days covered / 5
subjectHitRate = matched entries / total recognized lines

Threshold: β‰₯ 0.6 confidence AND β‰₯ 20 entries β†’ skip cloud call

Cloud Providers

  • Groq Llama 4 β€” endpoint: api.groq.com/openai/v1/chat/completions, model: meta-llama/llama-4-scout-17b-16e-instruct. Free key from console.groq.com.
  • Worker Proxy β€” endpoint: breakcount-ai.breakcount.workers.dev. Rate limit: 5 scans/day per device ID. No API key required.

Review Screen

AiReviewScreen shows entries day-by-day (Mon→Fri stepper). Features: edit subject name with autocomplete, pick start/end time, cycle color, swipe to delete, add new entries, and a green "Parsed offline" banner when isOfflineOcr is true.

Lock-Screen Live Activity

Android 14+ foreground service that shows a persistent break countdown notification on the lock screen. Uses Chronometer for battery-efficient live ticking.

Architecture

Flutter (LiveActivityService)
    ↓ MethodChannel 'com.breakcount/live_activity'
MainActivity.kt (channel handler)
    ↓ startForegroundService / stopService
BreakCountdownService.kt
    ↓ reads widget data from HomeWidget SharedPreferences
    ↓ builds Notification with Chronometer
Lock screen / notification shade

Method Channel API

Channel: com.breakcount/live_activity

MethodReturnsDescription
startboolStarts service (false if < API 34)
stopboolStops service
isRunningboolService active state
isAvailableboolDevice supports it (API 34+)

BreakCountdownService (Kotlin)

  • Foreground service type: specialUse
  • Notification channel: breakcount_countdown (IMPORTANCE_LOW)
  • Ongoing, silent, public visibility with Chronometer counting down to break start
  • Handler posts every 15 minutes to refresh notification data

Permissions

  • FOREGROUND_SERVICE β€” base permission
  • FOREGROUND_SERVICE_SPECIAL_USE β€” required for specialUse type on API 34+
  • POST_NOTIFICATIONS β€” required to show the notification

Nearby / Mesh System

BreakCount uses Android Nearby Connections (Bluetooth) for peer-to-peer features: schedule sharing, Vibe Beacon, and achievement compare.

Discovery Flow

startDiscovery() β†’ onPeerFound callback
    ↓
User taps peer card
    ↓
requestConnection() β†’ onConnectionEstablished
    ↓
sendPayload(data) ↔ receivePayload(data)
    ↓
disconnect()

Handshake Payload

{
  "type": "handshake",
  "name": "Anonymous Student",
  "persona_id": "hype",
  "persona_emoji": "πŸ”₯",
  "subject_count": 12,
  "entry_count": 35,
  "unlocked_achievement_ids": ["streak_7", "first_exam", ...]
}

Features

Schedule Copy

Peer card shows subject/entry count. "Copy" button pulls their schedule entries to your device. Merge/replace dialog if you already have entries.

Vibe Beacon

Long-press Vibe card β†’ radar overlay. Groups discovered peers by persona. Real-time discovery while overlay is open.

Achievement Compare

Computes three sets from the handshake payload: Both have (intersection), Only mine, and Only theirs. Displayed as a bottom sheet from the nearby users screen.

Shake to Share

ShakeService detects 20 m/sΒ² threshold. Opens share overlay when both devices shake simultaneously. Uses the same Nearby Connections infrastructure.

Privacy

  • No real names exchanged β€” anonymous display names only
  • Achievement IDs are opaque strings (no personal data)
  • Opt-in via Settings β†’ Social toggle (vibeBeaconEnabled)
  • Bluetooth/Location permissions required