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