diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7bc8cd5..ede6351 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,9 @@
tools:ignore="ScopedStorage" />
+
+
+
@@ -44,6 +47,11 @@
+
+
+
diff --git a/app/src/main/java/sh/sar/isodroid/MainActivity.kt b/app/src/main/java/sh/sar/isodroid/MainActivity.kt
index a2b1f83..660ae51 100644
--- a/app/src/main/java/sh/sar/isodroid/MainActivity.kt
+++ b/app/src/main/java/sh/sar/isodroid/MainActivity.kt
@@ -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 {
diff --git a/app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt b/app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt
new file mode 100644
index 0000000..a54c156
--- /dev/null
+++ b/app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt
@@ -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(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()
+}
diff --git a/app/src/main/java/sh/sar/isodroid/notification/NotificationHelper.kt b/app/src/main/java/sh/sar/isodroid/notification/NotificationHelper.kt
new file mode 100644
index 0000000..cb440e5
--- /dev/null
+++ b/app/src/main/java/sh/sar/isodroid/notification/NotificationHelper.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/sh/sar/isodroid/notification/UnmountReceiver.kt b/app/src/main/java/sh/sar/isodroid/notification/UnmountReceiver.kt
new file mode 100644
index 0000000..2a53ed8
--- /dev/null
+++ b/app/src/main/java/sh/sar/isodroid/notification/UnmountReceiver.kt
@@ -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()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
index c55e533..b5be1b0 100644
--- a/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/sh/sar/isodroid/viewmodel/MainViewModel.kt
@@ -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 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) {
diff --git a/app/src/main/res/drawable/ic_eject.xml b/app/src/main/res/drawable/ic_eject.xml
new file mode 100644
index 0000000..41a66b7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_eject.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..67e7605
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,10 @@
+
+
+