add notifcaiton support with unmount button
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<!-- Notification permission for Android 13+ -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Query for root shell packages -->
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -44,6 +47,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Broadcast receiver for unmount action from notification -->
|
||||
<receiver
|
||||
android:name=".notification.UnmountReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
22
app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt
Normal file
22
app/src/main/java/sh/sar/isodroid/isodrive/MountEventBus.kt
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
10
app/src/main/res/drawable/ic_eject.xml
Normal file
10
app/src/main/res/drawable/ic_eject.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M5,17h14v2H5zM12,5L5.33,15h13.34z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_notification.xml
Normal file
10
app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,9.5c-1.38,0 -2.5,1.12 -2.5,2.5s1.12,2.5 2.5,2.5 2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5z"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user