diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 221f7ca8f..9857d5303 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,6 +32,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.BCR">
+
+
{
+ val file = parseFileFromIntent(intent)
+
+ val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
+ if (notificationId < 0) {
+ throw IllegalArgumentException("Invalid notification ID: $notificationId")
+ }
+
+ return Pair(file, notificationId)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
+ try {
+ when (intent?.action) {
+ ACTION_DELETE_URI -> {
+ val (file, notificationId) = parseDeleteUriIntent(intent)
+ val documentFile = file.toDocumentFile(this)
+ val notificationManager = getSystemService(NotificationManager::class.java)
+
+ Thread {
+ Log.d(TAG, "Deleting: ${file.redacted}")
+ try {
+ documentFile.delete()
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to delete ${file.redacted}", e)
+ }
+
+ handler.post {
+ notificationManager.cancel(notificationId)
+ stopSelf(startId)
+ }
+ }.start()
+ }
+ else -> throw IllegalArgumentException("Invalid action: ${intent.action}")
+ }
+
+ START_REDELIVER_INTENT
+ } catch (e: Exception) {
+ val redactedIntent = intent?.let { Intent(it) }?.apply {
+ setDataAndType(Uri.fromParts("redacted", "", ""), type)
+ }
+
+ Log.w(TAG, "Failed to handle intent: $redactedIntent", e)
+ stopSelf(startId)
+
+ START_NOT_STICKY
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/chiller3/bcr/Notifications.kt b/app/src/main/java/com/chiller3/bcr/Notifications.kt
index 75891c6d3..77a949031 100644
--- a/app/src/main/java/com/chiller3/bcr/Notifications.kt
+++ b/app/src/main/java/com/chiller3/bcr/Notifications.kt
@@ -145,23 +145,23 @@ class Notifications(
}
/**
- * Create an alert notification with the given [title] and [icon].
+ * Send an alert notification with the given [title] and [icon].
*
* * If [errorMsg] is not null, then it is appended to the text with a black line before it.
* * If [file] is not null, the human-readable URI path is appended to the text with a blank
- * line before it if needed. In addition, two actions, open and share, are added to the
- * notification. Neither will dismiss the notification when clicked. Clicking on the
- * notification itself will behave like the open action, except the notification will be
- * dismissed.
+ * line before it if needed. In addition, three actions, open/share/delete, are added to the
+ * notification. The delete action dismisses the notification, but open and share do not.
+ * Clicking on the notification itself will behave like the open action, except the
+ * notification will be dismissed.
*/
- private fun createAlertNotification(
+ private fun sendAlertNotification(
channel: String,
@StringRes title: Int,
@DrawableRes icon: Int,
errorMsg: String?,
file: OutputFile?,
- ): Notification =
- Notification.Builder(context, channel).run {
+ ) {
+ val notification = Notification.Builder(context, channel).run {
val text = buildString {
val errorMsgTrimmed = errorMsg?.trim()
if (!errorMsgTrimmed.isNullOrBlank()) {
@@ -208,6 +208,12 @@ class Notifications(
},
PendingIntent.FLAG_IMMUTABLE,
)
+ val deleteIntent = PendingIntent.getService(
+ context,
+ 0,
+ NotificationActionService.createDeleteUriIntent(context, file, notificationId),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
addAction(Notification.Action.Builder(
null,
@@ -221,6 +227,12 @@ class Notifications(
shareIntent,
).build())
+ addAction(Notification.Action.Builder(
+ null,
+ context.getString(R.string.notification_action_delete),
+ deleteIntent,
+ ).build())
+
// Clicking on the notification behaves like the open action, except the
// notification gets dismissed. The open and share actions do not dismiss the
// notification.
@@ -231,8 +243,6 @@ class Notifications(
build()
}
- /** Send [notification] without overwriting prior alert notifications. */
- private fun notify(notification: Notification) {
notificationManager.notify(notificationId, notification)
++notificationId
}
@@ -249,7 +259,7 @@ class Notifications(
@DrawableRes icon: Int,
file: OutputFile,
) {
- notify(createAlertNotification(CHANNEL_ID_SUCCESS, title, icon, null, file))
+ sendAlertNotification(CHANNEL_ID_SUCCESS, title, icon, null, file)
vibrateIfEnabled(CHANNEL_ID_SUCCESS)
}
@@ -266,7 +276,7 @@ class Notifications(
errorMsg: String?,
file: OutputFile?,
) {
- notify(createAlertNotification(CHANNEL_ID_FAILURE, title, icon, errorMsg, file))
+ sendAlertNotification(CHANNEL_ID_FAILURE, title, icon, errorMsg, file)
vibrateIfEnabled(CHANNEL_ID_FAILURE)
}
diff --git a/app/src/main/java/com/chiller3/bcr/OutputFile.kt b/app/src/main/java/com/chiller3/bcr/OutputFile.kt
index 091b125f0..7bd134e03 100644
--- a/app/src/main/java/com/chiller3/bcr/OutputFile.kt
+++ b/app/src/main/java/com/chiller3/bcr/OutputFile.kt
@@ -1,8 +1,29 @@
package com.chiller3.bcr
+import android.content.ContentResolver
+import android.content.Context
import android.net.Uri
+import androidx.core.net.toFile
+import androidx.documentfile.provider.DocumentFile
data class OutputFile(
+ /**
+ * URI to a single file, which may have a [ContentResolver.SCHEME_FILE] or
+ * [ContentResolver.SCHEME_CONTENT] scheme.
+ */
val uri: Uri,
+
+ /** String representation of [uri] with private information redacted. */
+ val redacted: String,
+
+ /** MIME type of [uri]'s contents. */
val mimeType: String,
-)
+) {
+ fun toDocumentFile(context: Context): DocumentFile =
+ when (uri.scheme) {
+ ContentResolver.SCHEME_FILE -> DocumentFile.fromFile(uri.toFile())
+ // Only returns null on API <19
+ ContentResolver.SCHEME_CONTENT -> DocumentFile.fromSingleUri(context, uri)!!
+ else -> throw IllegalArgumentException("Invalid URI scheme: $redacted")
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt b/app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt
index a98038606..aa268b674 100644
--- a/app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt
+++ b/app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt
@@ -228,7 +228,7 @@ class RecorderInCallService : InCallService(), RecorderThread.OnRecordingComplet
}
override fun onRecordingCompleted(thread: RecorderThread, file: OutputFile) {
- Log.i(TAG, "Recording completed: ${thread.id}: ${thread.redact(file.uri)}")
+ Log.i(TAG, "Recording completed: ${thread.id}: ${file.redacted}")
handler.post {
onThreadExited()
@@ -237,7 +237,7 @@ class RecorderInCallService : InCallService(), RecorderThread.OnRecordingComplet
}
override fun onRecordingFailed(thread: RecorderThread, errorMsg: String?, file: OutputFile?) {
- Log.w(TAG, "Recording failed: ${thread.id}: ${file?.uri?.let { thread.redact(it) }}")
+ Log.w(TAG, "Recording failed: ${thread.id}: ${file?.redacted}")
handler.post {
onThreadExited()
diff --git a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
index 080a5a960..c698bc92c 100644
--- a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
+++ b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt
@@ -101,7 +101,7 @@ class RecorderThread(
}
}
- fun redact(uri: Uri): String = redact(Uri.decode(uri.toString()))
+ private fun redact(uri: Uri): String = redact(Uri.decode(uri.toString()))
/**
* Update [filename] with information from [details].
@@ -232,7 +232,7 @@ class RecorderThread(
Log.w(tag, "Failed to dump logcat", e)
}
- val outputFile = resultUri?.let { OutputFile(it, format.mimeTypeContainer) }
+ val outputFile = resultUri?.let { OutputFile(it, redact(it), format.mimeTypeContainer) }
if (success) {
listener.onRecordingCompleted(this, outputFile!!)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6d1695ac7..436478b93 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -51,6 +51,7 @@
Successfully recorded call
Open
Share
+ Delete
Call recording