From 93c239eb1299979b39f7e4c253e59ade079f5d01 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Tue, 10 Mar 2026 01:01:20 +0500 Subject: [PATCH] add notifcaiton support with unmount button --- .idea/deviceManager.xml | 13 +++ app/src/main/AndroidManifest.xml | 8 ++ .../main/java/sh/sar/isodroid/MainActivity.kt | 16 +++ .../sh/sar/isodroid/isodrive/MountEventBus.kt | 22 ++++ .../notification/NotificationHelper.kt | 109 ++++++++++++++++++ .../isodroid/notification/UnmountReceiver.kt | 43 +++++++ .../sar/isodroid/viewmodel/MainViewModel.kt | 28 +++++ app/src/main/res/drawable/ic_eject.xml | 10 ++ app/src/main/res/drawable/ic_notification.xml | 10 ++ 9 files changed, 259 insertions(+) create mode 100644 .idea/deviceManager.xml create mode 100644 app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt create mode 100644 app/src/main/java/sh/sar/isodroid/notification/NotificationHelper.kt create mode 100644 app/src/main/java/sh/sar/isodroid/notification/UnmountReceiver.kt create mode 100644 app/src/main/res/drawable/ic_eject.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml 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 @@ + + +