Initializing
Back to Projects
Year2024
DomainFullstack
AccessOpen Source
Complexity0 / 10
AndroidKotlinJetpack ComposeClean ArchitectureMVVMRoomHiltWorkManagerMaterial 3
FullstackArchived

AQMS - AI Agent Quota Management System

A lightweight, offline-first Android application for tracking AI service quotas (Gemini, Claude, GPT) with smart notifications when quota reset times are reached.

Parsing system architecture diagram...

Technology Stack

LayerTechnologyVersion/Details
LanguageKotlin2.0.0
UI FrameworkJetpack ComposeBOM 2024.05.00
Design SystemMaterial 31.2.1
ArchitectureClean Architecture + MVVM-
DatabaseRoom2.6.1
DI FrameworkHilt2.51.1
Background TasksWorkManager2.9.0
SecurityEncryptedSharedPreferences1.1.0-alpha06
BiometricsBiometric API1.2.0-alpha05
NavigationCompose Navigation2.7.7
Build SystemGradle (KTS)AGP 8.13.2

Purpose and Philosophy

AQMS (AI Agent Quota Management System) is a lightweight, offline-first Android application designed for power users of AI services such as Google Gemini, Anthropic Claude, and OpenAI GPT. The application allows users to manually track their daily or periodic usage quotas and receive smart push notifications exactly when their next quota window becomes available.

The core philosophy centers on "Privacy-First Intelligence" - all data remains local on the device, with no cloud dependency or remote telemetry. The app is designed for personal use with a minimalist approach, avoiding unnecessary features while maintaining professional-grade functionality.

Core Design Principles

  1. Offline-First Architecture: The application operates completely offline using local Room database storage. No internet connection is required for any functionality, ensuring reliability and privacy.
  1. User-Driven Data Entry: All usage data is manually entered by the user - date, time, and usage percentage. This provides flexibility for tracking various quota models (daily, weekly, monthly).
  1. Cloud-Sync Ready Schema: While currently offline-only, the database schema includes fields for future cloud synchronization: serverId, isSynced, isDeleted, and lastModifiedAt.
  1. Lightweight and Focused: The app avoids bloat, focusing solely on quota tracking and notification management without unnecessary features.
  1. Professional-Grade Security: Includes biometric authentication (fingerprint/face) and PIN lock with encrypted storage for sensitive data protection.

Architecture Deep Dive

Clean Architecture Layers

The project follows strict Clean Architecture principles with clear separation of concerns:

kotlin
// Domain Model - Pure Kotlin (no Android dependencies)
data class Account(
    val localId: Long = 0,
    val serverId: String? = null,
    val accountName: String,
    val accountType: String,
    val notes: String = "",
    val usages: List<ServiceUsage> = emptyList(),
    val createdAt: Long = System.currentTimeMillis(),
    val lastModifiedAt: Long = System.currentTimeMillis()
)
kotlin
// Domain Model - Service Usage
data class ServiceUsage(
    val localId: Long = 0,
    val accountLocalId: Long,
    val serverId: String? = null,
    val serviceType: ServiceType,
    val serviceLabel: String,
    val serviceTierTag: String = "",
    val usagePercent: Float = 0f,
    val usageLoggedAt: Long = System.currentTimeMillis(),
    val resetAt: Long? = null,
    val isResetNotified: Boolean = false,
    val createdAt: Long = System.currentTimeMillis(),
    val lastModifiedAt: Long = System.currentTimeMillis()
)

Service Type Enum

kotlin
enum class ServiceType(
    val displayName: String,
    val defaultTierTag: String
) {
    GEMINI_PRO("Gemini Pro", "Free Tier"),
    GEMINI_FLASH("Gemini Flash", "Free Tier"),
    CLAUDE("Claude", "Free Tier"),
    GPT("ChatGPT / GPT", "Free Tier"),
    CUSTOM("Custom Service", "");

    companion object {
        fun fromString(name: String): ServiceType =
            entries.firstOrNull { it.name == name } ?: CUSTOM
    }
}

Repository Pattern

kotlin
// Domain Repository Interface
interface AccountRepository {
    fun getAllAccounts(): Flow<List<Account>>
    suspend fun getAccountById(id: Long): Account?
    suspend fun addAccount(account: Account): Long
    suspend fun updateAccount(account: Account)
    suspend fun deleteAccount(id: Long)
}

interface ServiceUsageRepository {
    fun getUsagesForAccount(accountId: Long): Flow<List<ServiceUsage>>
    suspend fun addUsage(usage: ServiceUsage): Long
    suspend fun updateUsage(usage: ServiceUsage)
    suspend fun deleteUsage(id: Long)
    suspend fun getOverdueResets(currentTime: Long): List<ServiceUsage>
    suspend fun markResetNotified(id: Long)
}

Use Cases

Each use case implements single responsibility pattern:

kotlin
// Get All Accounts Use Case
class GetAllAccountsUseCase @Inject constructor(
    private val repository: AccountRepository
) {
    operator fun invoke(): Flow<List<Account>> = repository.getAllAccounts()
}

// Add Account Use Case with validation
class AddAccountUseCase @Inject constructor(
    private val repository: AccountRepository
) {
    suspend operator fun invoke(account: Account): Result<Long> = runCatching {
        require(account.accountName.isNotBlank()) { "Account name cannot be blank" }
        repository.addAccount(account)
    }
}

// Update Service Usage with validation
class UpdateServiceUsageUseCase @Inject constructor(
    private val repository: ServiceUsageRepository
) {
    suspend operator fun invoke(usage: ServiceUsage): Result<Unit> = runCatching {
        require(usage.usagePercent in 0f..100f) { "Usage must be between 0 and 100" }
        repository.updateUsage(
            usage.copy(
                usageLoggedAt = System.currentTimeMillis(),
                lastModifiedAt = System.currentTimeMillis(),
                isResetNotified = if (usage.resetAt != null) false else usage.isResetNotified
            )
        )
    }
}

// Get Accounts with Reset Due
class GetAccountsWithResetDueUseCase @Inject constructor(
    private val repository: ServiceUsageRepository
) {
    suspend operator fun invoke(): List<ServiceUsage> =
        repository.getOverdueResets(System.currentTimeMillis())
}

Database Design (Room)

Entity Definitions

kotlin
// Account Entity with Cloud-Sync Ready Fields
@Entity(tableName = "accounts")
data class AccountEntity(
    @PrimaryKey(autoGenerate = true)
    val localId: Long = 0,

    // Future cloud sync fields
    val serverId: String? = null,
    val isSynced: Boolean = false,
    val isDeleted: Boolean = false,

    // Core fields
    val accountName: String,
    val accountType: String,
    val notes: String = "",

    // Audit fields
    val createdAt: Long = System.currentTimeMillis(),
    val lastModifiedAt: Long = System.currentTimeMillis()
)
kotlin
// Service Usage Entity
@Entity(
    tableName = "service_usages",
    foreignKeys = [
        ForeignKey(
            entity = AccountEntity::class,
            parentColumns = ["localId"],
            childColumns = ["accountLocalId"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index(value = ["accountLocalId"])]
)
data class ServiceUsageEntity(
    @PrimaryKey(autoGenerate = true)
    val localId: Long = 0,

    val accountLocalId: Long,

    // Future cloud sync fields
    val serverId: String? = null,
    val isSynced: Boolean = false,
    val isDeleted: Boolean = false,

    // Service identity
    val serviceType: String,
    val serviceLabel: String,
    val serviceTierTag: String = "",

    // Usage tracking
    val usagePercent: Float = 0f,
    val usageLoggedAt: Long = System.currentTimeMillis(),

    // Reset tracking
    val resetAt: Long? = null,
    val isResetNotified: Boolean = false,

    // Audit fields
    val createdAt: Long = System.currentTimeMillis(),
    val lastModifiedAt: Long = System.currentTimeMillis()
)
kotlin
// Error Log Entity for local crash reporting
@Entity(tableName = "error_logs")
data class ErrorLogEntity(
    @PrimaryKey(autoGenerate = true)
    val localId: Long = 0,

    val tag: String,
    val message: String,
    val stackTrace: String = "",
    val severity: String,
    val occurredAt: Long = System.currentTimeMillis()
)

DAO Interfaces

kotlin
@Dao
interface AccountDao {
    @Query("SELECT * FROM accounts WHERE isDeleted = 0 ORDER BY lastModifiedAt DESC")
    fun getAllAccounts(): Flow<List<AccountEntity>>

    @Query("SELECT * FROM accounts WHERE localId = :id AND isDeleted = 0")
    suspend fun getAccountById(id: Long): AccountEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAccount(account: AccountEntity): Long

    @Update
    suspend fun updateAccount(account: AccountEntity)

    // Soft delete for sync safety
    @Query("UPDATE accounts SET isDeleted = 1, lastModifiedAt = :timestamp WHERE localId = :id")
    suspend fun softDeleteAccount(id: Long, timestamp: Long = System.currentTimeMillis())

    // For future cloud sync
    @Query("SELECT * FROM accounts WHERE isSynced = 0 AND isDeleted = 0")
    suspend fun getUnsyncedAccounts(): List<AccountEntity>
}
kotlin
@Dao
interface ServiceUsageDao {
    @Query("""
        SELECT * FROM service_usages 
        WHERE accountLocalId = :accountId AND isDeleted = 0 
        ORDER BY serviceType ASC
    """)
    fun getUsagesForAccount(accountId: Long): Flow<List<ServiceUsageEntity>>

    @Query("""
        SELECT * FROM service_usages 
        WHERE isDeleted = 0 
        AND resetAt IS NOT NULL 
        AND resetAt <= :currentTime 
        AND isResetNotified = 0
    """)
    suspend fun getOverdueResets(currentTime: Long): List<ServiceUsageEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsage(usage: ServiceUsageEntity): Long

    @Update
    suspend fun updateUsage(usage: ServiceUsageEntity)

    @Query("UPDATE service_usages SET isResetNotified = 1 WHERE localId = :id")
    suspend fun markResetNotified(id: Long)

    @Query("SELECT * FROM service_usages WHERE isSynced = 0 AND isDeleted = 0")
    suspend fun getUnsyncedUsages(): List<ServiceUsageEntity>
}

Database Class

kotlin
@Database(
    entities = [
        AccountEntity::class,
        ServiceUsageEntity::class,
        ErrorLogEntity::class
    ],
    version = 1,
    exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AQMSDatabase : RoomDatabase() {
    abstract fun accountDao(): AccountDao
    abstract fun serviceUsageDao(): ServiceUsageDao
    abstract fun errorLogDao(): ErrorLogDao

    companion object {
        const val DATABASE_NAME = "aqms_database"
    }
}

Presentation Layer

ViewModel Pattern

kotlin
@HiltViewModel
class DashboardViewModel @Inject constructor(
    private val getAllAccounts: GetAllAccountsUseCase,
    private val deleteAccount: DeleteAccountUseCase
) : ViewModel() {

    data class UiState(
        val accounts: List<Account> = emptyList(),
        val isLoading: Boolean = true,
        val sortOrder: SortOrder = SortOrder.LAST_MODIFIED,
        val filterType: String? = null,
        val error: String? = null
    )

    enum class SortOrder { LAST_MODIFIED, NAME_ASC, NAME_DESC, USAGE_HIGH }

    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    init {
        loadAccounts()
    }

    private fun loadAccounts() {
        viewModelScope.launch {
            getAllAccounts()
                .catch { e -> _uiState.update { it.copy(error = e.message, isLoading = false) } }
                .collect { accounts ->
                    _uiState.update { state ->
                        state.copy(
                            accounts = accounts.applySort(state.sortOrder),
                            isLoading = false
                        )
                    }
                }
        }
    }

    fun setSortOrder(order: SortOrder) {
        _uiState.update { it.copy(sortOrder = order, accounts = it.accounts.applySort(order)) }
    }

    fun deleteAccount(id: Long) {
        viewModelScope.launch {
            deleteAccount(id).onFailure { e ->
                _uiState.update { it.copy(error = e.message) }
            }
        }
    }

    private fun List<Account>.applySort(order: SortOrder) = when (order) {
        SortOrder.LAST_MODIFIED -> sortedByDescending { it.lastModifiedAt }
        SortOrder.NAME_ASC      -> sortedBy { it.accountName }
        SortOrder.NAME_DESC    -> sortedByDescending { it.accountName }
        SortOrder.USAGE_HIGH   -> sortedByDescending { account ->
            account.usages.maxOfOrNull { it.usagePercent } ?: 0f
        }
    }
}

Usage Thresholds System

kotlin
object UsageThresholds {
    val LOW_THRESHOLD     = 0f..49f
    val MEDIUM_THRESHOLD = 50f..74f
    val HIGH_THRESHOLD   = 75f..89f
    val CRITICAL_THRESHOLD = 90f..99f
    val EXHAUSTED        = 100f

    fun getColor(percent: Float): Color = when {
        percent >= 100f -> Color(0xFFD32F2F)   // Red
        percent >= 90f  -> Color(0xFFE64A19)   // Deep Orange
        percent >= 75f  -> Color(0xFFF57C00)   // Orange
        percent >= 50f  -> Color(0xFFFBC02D)   // Yellow
        else            -> Color(0xFF388E3C)   // Green
    }

    fun getLabel(percent: Float): String = when {
        percent >= 100f -> "Exhausted"
        percent >= 90f  -> "Critical"
        percent >= 75f  -> "High"
        percent >= 50f  -> "Moderate"
        else            -> "Available"
    }
}

Compose Components

kotlin
@Composable
fun UsageIndicator(
    percent: Float,
    modifier: Modifier = Modifier,
    size: Dp = 56.dp,
    strokeWidth: Dp = 5.dp
) {
    val color = UsageThresholds.getColor(percent)
    val animatedProgress by animateFloatAsState(
        targetValue = percent / 100f,
        animationSpec = tween(800, easing = FastOutSlowInEasing),
        label = "usage_progress"
    )

    Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
        CircularProgressIndicator(
            progress = { animatedProgress },
            color = color,
            strokeWidth = strokeWidth,
            trackColor = color.copy(alpha = 0.15f)
        )
        Text(
            text = "${percent.toInt()}%",
            style = MaterialTheme.typography.labelSmall,
            color = color,
            fontWeight = FontWeight.Bold
        )
    }
}

Background Processing (WorkManager)

Quota Reset Worker

kotlin
@HiltWorker
class QuotaResetWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val getResetsDue: GetAccountsWithResetDueUseCase,
    private val usageRepository: ServiceUsageRepository,
    private val notificationHelper: NotificationHelper,
    private val errorLogRepository: ErrorLogRepository
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            val overdueUsages = getResetsDue()

            if (overdueUsages.isEmpty()) return Result.success()

            val byAccount = overdueUsages.groupBy { it.accountLocalId }

            if (byAccount.size == 1) {
                overdueUsages.forEach { usage ->
                    notificationHelper.sendResetNotification(usage)
                }
            } else {
                notificationHelper.sendGroupedResetNotification(overdueUsages)
                overdueUsages.forEach { usage ->
                    notificationHelper.sendResetNotification(usage, inGroup = true)
                }
            }

            overdueUsages.forEach { usageRepository.markResetNotified(it.localId) }

            Result.success()
        } catch (e: Exception) {
            errorLogRepository.log(
                tag = "QuotaResetWorker",
                message = "Worker failed: ${e.message}",
                stackTrace = e.stackTraceToString(),
                severity = "ERROR"
            )
            if (runAttemptCount < 3) Result.retry() else Result.failure()
        }
    }

    companion object {
        const val WORK_NAME = "quota_reset_check"
    }
}

Worker Scheduler

kotlin
class WorkerScheduler @Inject constructor(
    private val context: Context
) {
    fun schedulePeriodicQuotaCheck(intervalHours: Int = 1) {
        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(false)
            .build()

        val request = PeriodicWorkRequestBuilder<QuotaResetWorker>(
            repeatInterval = intervalHours.toLong(),
            repeatIntervalTimeUnit = TimeUnit.HOURS
        )
            .setConstraints(constraints)
            .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES)
            .build()

        WorkManager.getInstance(context).enqueueUniquePeriodicWork(
            QuotaResetWorker.WORK_NAME,
            ExistingPeriodicWorkPolicy.UPDATE,
            request
        )
    }

    fun cancelAll() {
        WorkManager.getInstance(context).cancelUniqueWork(QuotaResetWorker.WORK_NAME)
    }
}

Notifications

Notification Channels

kotlin
object NotificationChannels {
    const val QUOTA_RESET_CHANNEL_ID   = "quota_reset_channel"
    const val QUOTA_RESET_CHANNEL_NAME = "Quota Reset Alerts"

    fun createChannels(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                QUOTA_RESET_CHANNEL_ID,
                QUOTA_RESET_CHANNEL_NAME,
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Notifications when AI quota resets are available"
                enableLights(true)
                enableVibration(true)
            }
            val manager = context.getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }
}

Notification Helper

kotlin
class NotificationHelper @Inject constructor(
    private val context: Context,
    private val accountRepository: AccountRepository
) {
    suspend fun sendResetNotification(usage: ServiceUsage, inGroup: Boolean = false) {
        val account = accountRepository.getAccountById(usage.accountLocalId)
        val accountName = account?.accountName ?: "Unknown Account"

        val builder = NotificationCompat.Builder(context, QUOTA_RESET_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_quota)
            .setContentTitle("Quota Reset Available")
            .setContentText("${usage.serviceLabel} is available for $accountName")
            .setStyle(NotificationCompat.BigTextStyle()
                .bigText("Your ${usage.serviceLabel} quota for $accountName has reset."))
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setAutoCancel(true)
            .setContentIntent(buildOpenAppIntent(usage.accountLocalId))

        if (inGroup) {
            builder.setGroup(GROUP_KEY_RESETS)
        }

        NotificationManagerCompat.from(context)
            .notify(usage.localId.toInt(), builder.build())
    }

    fun sendGroupedResetNotification(usages: List<ServiceUsage>) {
        val summary = NotificationCompat.Builder(context, QUOTA_RESET_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_quota)
            .setContentTitle("${usages.size} Quota Resets Available")
            .setContentText("Multiple AI services have reset their quotas")
            .setStyle(NotificationCompat.InboxStyle()
                .also { style -> usages.take(5).forEach { usage ->
                    style.addLine("• ${usage.serviceLabel}")
                }}
                .setSummaryText("${usages.size} services reset"))
            .setGroup(GROUP_KEY_RESETS)
            .setGroupSummary(true)
            .setAutoCancel(true)
            .build()

        NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, summary)
    }

    companion object {
        const val GROUP_KEY_RESETS = "com.aqms.QUOTA_RESETS"
        const val SUMMARY_NOTIFICATION_ID = 9999
    }
}

Security Implementation

PIN Manager with EncryptedSharedPreferences

kotlin
class PinManager @Inject constructor(
    @ApplicationContext context: Context
) {
    private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
        context,
        "aqms_secure_prefs",
        MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    fun setPin(pin: String) {
        require(pin.length == 4 && pin.all { it.isDigit() }) { "PIN must be exactly 4 digits" }
        prefs.edit().putString(KEY_PIN, pin.hashCode().toString()).apply()
        prefs.edit().putBoolean(KEY_PIN_ENABLED, true).apply()
    }

    fun verifyPin(input: String): Boolean =
        prefs.getString(KEY_PIN, null) == input.hashCode().toString()

    fun isPinEnabled(): Boolean = prefs.getBoolean(KEY_PIN_ENABLED, false)

    fun clearPin() {
        prefs.edit().remove(KEY_PIN).putBoolean(KEY_PIN_ENABLED, false).apply()
    }

    companion object {
        private const val KEY_PIN = "app_pin"
        private const val KEY_PIN_ENABLED = "pin_enabled"
    }
}

Security Flow

code
App Launch
Check isPinEnabled()
YES: Show PinLockScreen (4-digit keypad)
User enters PINverifyPin()
Correct: navigate to Dashboard
Wrong: shake animation + attempt counter (max 5, then 30s lockout)
NO: Navigate directly to Dashboard

Dependency Injection (Hilt)

kotlin
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AQMSDatabase =
        Room.databaseBuilder(context, AQMSDatabase::class.java, AQMSDatabase.DATABASE_NAME)
            .fallbackToDestructiveMigrationOnDowngrade()
            .build()

    @Provides fun provideAccountDao(db: AQMSDatabase): AccountDao = db.accountDao()
    @Provides fun provideServiceUsageDao(db: AQMSDatabase): ServiceUsageDao = db.serviceUsageDao()
    @Provides fun provideErrorLogDao(db: AQMSDatabase): ErrorLogDao = db.errorLogDao()
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds @Singleton
    abstract fun bindAccountRepository(impl: AccountRepositoryImpl): AccountRepository

    @Binds @Singleton
    abstract fun bindServiceUsageRepository(impl: ServiceUsageRepositoryImpl): ServiceUsageRepository
}

Features (v0.4.0)

Core Features

  • Account Management: Add, edit, delete AI service accounts
  • Usage Tracking: Manual quota usage logging (0-100%)
  • Service Types: Support for Gemini Pro, Gemini Flash, Claude, GPT, and custom services
  • Reset Scheduling: User-defined quota reset times with notifications

v0.4.0 Professional Features

  • Batch Actions: Reset all service quotas for an account in one tap
  • CSV Data Export: Export historical usage logs for external analysis
  • Biometric Security: Fingerprint/Face ID with PIN fallback
  • Search & Sort: Reactive search and sorting for power users with 10+ accounts
  • Usage Trends: Sparkline visualization for activity patterns
  • Dynamic Thresholds: User-defined color points for status indicators
  • Privacy First: 100% local, encrypted storage

Visual Design System

Material 3 Color Palette (Dark Mode First)

kotlin
object AQMSColors {
    // Primary brand — Electric Indigo
    val Primary       = Color(0xFF7C6FFF)
    val OnPrimary     = Color(0xFF1A1A2E)
    val PrimaryContainer = Color(0xFF3A3490)

    // Surface — Deep matte dark
    val Background    = Color(0xFF0E0E1A)
    val Surface       = Color(0xFF181828)
    val SurfaceVariant = Color(0xFF242438)
    val Outline       = Color(0xFF4A4A6A)

    // Semantic usage colors
    val UsageGreen    = Color(0xFF4CAF50)
    val UsageYellow   = Color(0xFFFFC107)
    val UsageOrange   = Color(0xFFFF9800)
    val UsageDeepRed  = Color(0xFFFF5722)
    val UsageRed      = Color(0xFFD32F2F)
}

Project Structure

code
app/src/main/kotlin/com.aqms/
├── AQMSApplication.kt
├── data/
│   ├── local/
│   │   ├── database/
│   │   │   ├── AQMSDatabase.kt
│   │   │   └── Converters.kt
│   │   ├── entity/
│   │   │   ├── AccountEntity.kt
│   │   │   ├── ServiceUsageEntity.kt
│   │   │   └── ErrorLogEntity.kt
│   │   └── dao/
│   │       ├── AccountDao.kt
│   │       ├── ServiceUsageDao.kt
│   │       └── ErrorLogDao.kt
│   └── repository/
│       ├── AccountRepositoryImpl.kt
│       ├── ServiceUsageRepositoryImpl.kt
│       └── ErrorLogRepositoryImpl.kt
├── domain/
│   ├── model/
│   │   ├── Account.kt
│   │   ├── ServiceUsage.kt
│   │   └── ServiceType.kt
│   ├── repository/
│   │   ├── AccountRepository.kt
│   │   └── ServiceUsageRepository.kt
│   └── usecase/
│       ├── account/
│       ├── usage/
│       └── notification/
├── presentation/
│   ├── navigation/
│   │   ├── NavGraph.kt
│   │   └── Screen.kt
│   ├── theme/
│   ├── components/
│   │   ├── AccountCard.kt
│   │   ├── UsageSlider.kt
│   │   ├── UsageIndicator.kt
│   │   ├── ServiceChip.kt
│   │   └── PinLockScreen.kt
│   └── screen/
│       ├── dashboard/
│       ├── detail/
│       ├── addedit/
│       └── settings/
├── worker/
│   ├── QuotaResetWorker.kt
│   └── WorkerScheduler.kt
├── notification/
│   ├── NotificationHelper.kt
│   └── NotificationChannels.kt
├── di/
│   ├── DatabaseModule.kt
│   ├── RepositoryModule.kt
│   └── UseCaseModule.kt
├── security/
│   └── PinManager.kt
└── utils/
    ├── DateTimeUtils.kt
    ├── UsageThresholds.kt
    ├── Extensions.kt
    └── Constants.kt

Build and Run

bash
# Build and run on emulator
./run_aqms.sh

# Or manually
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

Future Roadmap

  • Phase 10: Sync & Backup (Future scope)
  • Cloud synchronization capability
  • Cross-device backup
  • Data export/import

Conclusion

AQMS represents a comprehensive Android application demonstrating modern mobile development best practices. The project showcases Clean Architecture implementation with clear separation between UI, business logic, and data layers. Key achievements include:

  • Professional-grade offline-first data management
  • Background task scheduling with WorkManager
  • Smart notification system for quota resets
  • Security features including biometric authentication and encrypted storage
  • Material 3 design with custom theming
  • Reactive state management with Kotlin Flow and StateFlow

The architecture is designed for future scalability, with database schemas ready for cloud synchronization when needed. The minimalist approach ensures the app remains lightweight while providing powerful quota management capabilities for AI service power users.

(End of file - 695 lines)

Architecture Feedback

Spotted a potential optimization or antipattern? Let me know.

Submit a Technical Suggestion