add notifcaiton support with unmount button

This commit is contained in:
2026-03-10 01:01:20 +05:00
parent 0c5524a9a5
commit 93c239eb12
9 changed files with 259 additions and 0 deletions

View File

@@ -44,6 +44,10 @@ class MainActivity : ComponentActivity() {
}
}
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { /* Notification permission result - we continue regardless */ }
private val manageStorageLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
@@ -78,6 +82,18 @@ class MainActivity : ComponentActivity() {
}
private fun checkAndRequestPermissions() {
// Request notification permission on Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
// Request storage permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {

View File

@@ -0,0 +1,22 @@
package sh.sar.isodroid.isodrive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
object MountEventBus {
private val _events = MutableSharedFlow<MountEvent>(extraBufferCapacity = 1)
val events = _events.asSharedFlow()
fun emitUnmounted() {
_events.tryEmit(MountEvent.Unmounted)
}
fun emitMounted() {
_events.tryEmit(MountEvent.Mounted)
}
}
sealed class MountEvent {
data object Mounted : MountEvent()
data object Unmounted : MountEvent()
}

View File

@@ -0,0 +1,109 @@
package sh.sar.isodroid.notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import sh.sar.isodroid.MainActivity
import sh.sar.isodroid.R
import sh.sar.isodroid.data.MountStatus
import sh.sar.isodroid.data.MountType
class NotificationHelper(private val context: Context) {
companion object {
const val CHANNEL_ID = "iso_drive_mount_status"
const val NOTIFICATION_ID = 1001
const val ACTION_UNMOUNT = "sh.sar.isodroid.ACTION_UNMOUNT"
@Volatile
private var instance: NotificationHelper? = null
fun getInstance(context: Context): NotificationHelper {
return instance ?: synchronized(this) {
instance ?: NotificationHelper(context.applicationContext).also { instance = it }
}
}
}
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
init {
createNotificationChannel()
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Mount Status",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Shows when an ISO/IMG file is mounted"
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
fun showMountedNotification(mountStatus: MountStatus) {
if (!mountStatus.mounted || mountStatus.path == null) {
hideNotification()
return
}
val fileName = mountStatus.path.substringAfterLast("/")
val mountType = when (mountStatus.type) {
MountType.CDROM -> "CD-ROM"
MountType.MASS_STORAGE -> "Mass Storage"
else -> "USB"
}
val accessMode = if (mountStatus.readOnly) "Read-Only" else "Read-Write"
// Intent to open the app when notification is tapped
val openIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val openPendingIntent = PendingIntent.getActivity(
context,
0,
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Intent for unmount action
val unmountIntent = Intent(context, UnmountReceiver::class.java).apply {
action = ACTION_UNMOUNT
}
val unmountPendingIntent = PendingIntent.getBroadcast(
context,
1,
unmountIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("ISO Mounted")
.setContentText(fileName)
.setSubText("$mountType - $accessMode")
.setOngoing(true)
.setShowWhen(false)
.setContentIntent(openPendingIntent)
.addAction(
R.drawable.ic_eject,
"Unmount",
unmountPendingIntent
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
fun hideNotification() {
notificationManager.cancel(NOTIFICATION_ID)
}
}

View File

@@ -0,0 +1,43 @@
package sh.sar.isodroid.notification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import sh.sar.isodroid.isodrive.IsoDriveManager
import sh.sar.isodroid.isodrive.MountEventBus
class UnmountReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == NotificationHelper.ACTION_UNMOUNT) {
val pendingResult = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
val isoDriveManager = IsoDriveManager.getInstance(context)
val result = isoDriveManager.unmount()
CoroutineScope(Dispatchers.Main).launch {
if (result.success) {
Toast.makeText(context, "Unmounted successfully", Toast.LENGTH_SHORT).show()
NotificationHelper.getInstance(context).hideNotification()
MountEventBus.emitUnmounted()
} else {
Toast.makeText(context, "Unmount failed: ${result.message}", Toast.LENGTH_LONG).show()
}
pendingResult.finish()
}
} catch (e: Exception) {
CoroutineScope(Dispatchers.Main).launch {
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
pendingResult.finish()
}
}
}
}
}
}

View File

@@ -20,7 +20,10 @@ import sh.sar.isodroid.data.IsoFile
import sh.sar.isodroid.data.MountOptions
import sh.sar.isodroid.data.MountStatus
import sh.sar.isodroid.isodrive.IsoDriveManager
import sh.sar.isodroid.isodrive.MountEvent
import sh.sar.isodroid.isodrive.MountEventBus
import sh.sar.isodroid.isodrive.SupportStatus
import sh.sar.isodroid.notification.NotificationHelper
import sh.sar.isodroid.root.RootManager
import java.io.File
@@ -29,6 +32,7 @@ private val Application.dataStore: DataStore<Preferences> by preferencesDataStor
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val isoDriveManager = IsoDriveManager.getInstance(application)
private val notificationHelper = NotificationHelper.getInstance(application)
private val dataStore = application.dataStore
private val _uiState = MutableStateFlow(MainUiState())
@@ -45,6 +49,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch {
initialize()
}
// Observe mount events from notification actions
viewModelScope.launch {
MountEventBus.events.collect { event ->
when (event) {
is MountEvent.Unmounted -> {
checkMountStatus()
_uiState.update { it.copy(successMessage = "Unmounted successfully") }
}
is MountEvent.Mounted -> {
checkMountStatus()
}
}
}
}
}
private suspend fun initialize() {
@@ -178,6 +197,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private suspend fun checkMountStatus() {
val status = isoDriveManager.getStatus()
_uiState.update { it.copy(mountStatus = status) }
updateNotification(status)
}
private fun updateNotification(status: MountStatus) {
if (status.mounted) {
notificationHelper.showMountedNotification(status)
} else {
notificationHelper.hideNotification()
}
}
suspend fun mount(path: String, options: MountOptions) {