┌──────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ SettingsScreen │──│ Navigation │──│ DownloadsScreen │ │
│ │ (Compose) │ │ (NavHost) │ │ (Compose) │ │
│ └────────────────┘ └──────────────┘ └─────────────────┘ │
│ │ │
└──────────────────────────────────────────────┼────────────────┘
│
┌──────────────────────────────────────────────┼────────────────┐
│ Business Logic │ │
│ ┌────────────────────────────▼──────────┐ │
│ │ DownloadManagerHelper │ │
│ │ - startDownload() │ │
│ │ - updateDownloadStatus() │ │
│ │ - deleteDownload() │ │
│ │ - openFile() │ │
│ │ - shareFile() │ │
│ │ - clearAllDownloads() │ │
│ └───────────────────┬───────────────────┘ │
│ │ │
└──────────────────────────────────────┼─────────────────────────┘
│
┌──────────────────────────────────────┼─────────────────────────┐
│ Data Layer │ │
│ ┌────────────────┐ ┌───────────────▼──────┐ ┌────────────┐│
│ │ DownloadItem │ │ SharedPreferences │ │ FileSystem ││
│ │ (Data Class) │ │ (JSON Storage) │ │ (Downloads)││
│ └────────────────┘ └──────────────────────┘ └────────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘
Location: data/DownloadItem.kt
data class DownloadItem(
val id: Long, // Unique identifier
val filename: String, // Display name
val url: String, // Source URL
val mimeType: String?, // MIME type (e.g., "application/pdf")
val size: Long, // Size in bytes
val timestamp: Long, // Download start time
val status: DownloadStatus,// Current status
val filePath: String?, // Local file URI
val downloadId: Long // Android DownloadManager ID
)
enum class DownloadStatus {
PENDING,
DOWNLOADING,
COMPLETED,
FAILED,
CANCELLED
}Extension Function:
fun Long.toReadableSize(): String
// Converts bytes to human-readable format (B, KB, MB, GB, TB)Location: utils/DownloadManagerHelper.kt
Key Properties:
private val downloadManager: DownloadManager
private val prefs: SharedPreferences
private val _downloads: MutableStateFlow<List<DownloadItem>>
val downloads: StateFlow<List<DownloadItem>>Public Methods:
// Start a new download
fun startDownload(
url: String,
filename: String? = null,
mimeType: String? = null,
showNotification: Boolean = true
): Long
// Delete a download (file and metadata)
fun deleteDownload(downloadItem: DownloadItem)
// Open file with default app
fun openFile(downloadItem: DownloadItem)
// Share file via Android share sheet
fun shareFile(downloadItem: DownloadItem)
// Clear all downloads
fun clearAllDownloads()Private Methods:
// Update download status from DownloadManager
private fun updateDownloadStatus(downloadId: Long)
// Add new download to list
private fun addDownload(downloadItem: DownloadItem)
// Persist downloads to SharedPreferences
private fun saveDownloads()
// Load downloads from SharedPreferences
private fun loadDownloads()
// Register BroadcastReceiver for download completion
private fun registerDownloadReceiver()Location: ui/screens/DownloadsScreen.kt
Main Composable:
@Composable
fun DownloadsScreen(
onBackClick: () -> Unit
)Sub-components:
@Composable
fun DownloadItemCard(
downloadItem: DownloadItem,
onOpen: () -> Unit,
onShare: () -> Unit,
onDelete: () -> Unit
)State Management:
val downloadManager = remember { DownloadManagerHelper(context) }
val downloads by downloadManager.downloads.collectAsState()
var showDeleteDialog by remember { mutableStateOf(false) }
var selectedDownload by remember { mutableStateOf<DownloadItem?>(null) }Location: ui/screens/WebViewScreen.kt
Integration Point:
setDownloadListener(object : DownloadListener {
override fun onDownloadStart(
url: String?,
userAgent: String?,
contentDisposition: String?,
mimetype: String?,
contentLength: Long
) {
// Security check
if (securityLevel == SecurityLevel.HIGH) {
// Show error and return
return
}
// Extract filename
val filename = URLUtil.guessFileName(url, contentDisposition, mimetype)
// Start download with tracking
val downloadId = downloadManagerHelper.startDownload(
url = url,
filename = filename,
mimeType = mimetype,
showNotification = true
)
}
})WebView Download Triggered
↓
WebViewScreen.DownloadListener
↓
DownloadManagerHelper.startDownload()
↓
Android DownloadManager.enqueue()
↓
Create DownloadItem with DOWNLOADING status
↓
Update StateFlow
↓
Save to SharedPreferences
↓
UI updates automatically (via StateFlow)
Android DownloadManager completion
↓
BroadcastReceiver receives intent
↓
DownloadManagerHelper.updateDownloadStatus()
↓
Query DownloadManager for status
↓
Update DownloadItem with COMPLETED status
↓
Update StateFlow
↓
Save to SharedPreferences
↓
UI updates automatically
User taps "Open" or "Share"
↓
DownloadsScreen calls helper method
↓
DownloadManagerHelper.openFile() or shareFile()
↓
Create FileProvider URI
↓
Launch Intent with URI
↓
Android handles app selection
{
"downloads_list": [
{
"id": 1700000000000,
"filename": "document.pdf",
"url": "https://example.com/doc.pdf",
"mimeType": "application/pdf",
"size": 2621440,
"timestamp": 1700000000000,
"status": "COMPLETED",
"filePath": "file:///storage/emulated/0/Download/document.pdf",
"downloadId": 12345
}
]
}- Location:
/storage/emulated/0/Download/(public Downloads folder) - Access: Via FileProvider URIs for security
- Permissions: Scoped storage on Android 13+, legacy permissions on older versions
when (securityLevel) {
SecurityLevel.HIGH -> {
// Downloads disabled
showError("Downloads are disabled in High Security mode")
}
SecurityLevel.MEDIUM, SecurityLevel.LOW -> {
// Downloads allowed
startDownload(...)
}
}- Uses
androidx.core.content.FileProvider - Grants temporary URI permissions
- Prevents direct file path exposure
- Configured in
file_paths.xml
<!-- Only for Android 12 and below -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />try {
val downloadId = downloadManager.enqueue(request)
// Track download
} catch (e: Exception) {
Toast.makeText(context, "Download failed: ${e.message}", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Download error", e)
}try {
val uri = FileProvider.getUriForFile(context, authority, file)
// Open or share file
} catch (e: Exception) {
Log.e(TAG, "Failed to access file: ${e.message}", e)
}try {
val jsonArray = JSONArray(jsonString)
// Parse downloads
} catch (e: Exception) {
Log.e(TAG, "Failed to parse downloads: ${e.message}", e)
}// Test DownloadItem data class
@Test
fun testDownloadItemCreation() {
val item = DownloadItem(
filename = "test.pdf",
url = "https://example.com/test.pdf",
status = DownloadStatus.COMPLETED
)
assertEquals("test.pdf", item.filename)
assertEquals(DownloadStatus.COMPLETED, item.status)
}
// Test size formatting
@Test
fun testReadableSize() {
assertEquals("1.00 KB", 1024L.toReadableSize())
assertEquals("1.00 MB", (1024L * 1024).toReadableSize())
}// Test download flow
@Test
fun testDownloadFlow() {
val helper = DownloadManagerHelper(context)
val downloadId = helper.startDownload(
url = "https://example.com/test.pdf",
filename = "test.pdf"
)
assertTrue(downloadId > 0)
}@Test
fun testDownloadsScreen() {
composeTestRule.setContent {
DownloadsScreen(onBackClick = {})
}
// Verify empty state
composeTestRule
.onNodeWithText("No downloads yet")
.assertIsDisplayed()
}- StateFlow vs LiveData: Using StateFlow for reactive UI updates
- JSON vs Room: SharedPreferences with JSON for simplicity (suitable for moderate data)
- Lazy Loading: LazyColumn for efficient list rendering
- Background Operations: DownloadManager handles downloads in background
@Entity(tableName = "downloads")
data class DownloadEntity(
@PrimaryKey val id: Long,
val filename: String,
val url: String,
// ... other fields
)
@Dao
interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY timestamp DESC")
fun getAllDownloads(): Flow<List<DownloadEntity>>
@Insert
suspend fun insert(download: DownloadEntity)
@Delete
suspend fun delete(download: DownloadEntity)
}- Enable logging: Look for TAG "DownloadManagerHelper" in logcat
- Check file paths: Verify files exist at reported paths
- Inspect SharedPreferences: Use Device File Explorer
- Monitor DownloadManager: Query DownloadManager for status
- Test on real device: Emulator may have different behavior