Technology Stack
| Layer | Technology | Version/Details |
|---|---|---|
| Language | Kotlin | 2.0.0 |
| UI Framework | Jetpack Compose | BOM 2024.05.00 |
| Design System | Material 3 | 1.2.1 |
| Architecture | Clean Architecture + MVVM | - |
| Database | Room | 2.6.1 |
| DI Framework | Hilt | 2.51.1 |
| Background Tasks | WorkManager | 2.9.0 |
| Security | EncryptedSharedPreferences | 1.1.0-alpha06 |
| Biometrics | Biometric API | 1.2.0-alpha05 |
| Navigation | Compose Navigation | 2.7.7 |
| Build System | Gradle (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
- 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.
- 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).
- Cloud-Sync Ready Schema: While currently offline-only, the database schema includes fields for future cloud synchronization: serverId, isSynced, isDeleted, and lastModifiedAt.
- Lightweight and Focused: The app avoids bloat, focusing solely on quota tracking and notification management without unnecessary features.
- 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:
// 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()
)// 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
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
// 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:
// 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
// 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()
)// 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()
)// 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
@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>
}@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
@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
@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
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
@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
@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
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
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
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
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
App Launch
→ Check isPinEnabled()
→ YES: Show PinLockScreen (4-digit keypad)
→ User enters PIN → verifyPin()
→ Correct: navigate to Dashboard
→ Wrong: shake animation + attempt counter (max 5, then 30s lockout)
→ NO: Navigate directly to DashboardDependency Injection (Hilt)
@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)
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
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.ktBuild and Run
# Build and run on emulator
./run_aqms.sh
# Or manually
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apkFuture 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.