Skip to content

Commit

Permalink
Add a new notification action for deleting the recording
Browse files Browse the repository at this point in the history
The new Delete action will show up alongside the current Open and Share
actions, but unlike those other two actions, Delete will also dismiss
the notification.

Fixes: #179

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Nov 28, 2022
1 parent 62d969c commit df042ba
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 17 deletions.
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.BCR">

<service
android:name=".NotificationActionService"
android:exported="false" />

<service
android:name=".RecorderInCallService"
android:enabled="true"
Expand Down
92 changes: 92 additions & 0 deletions app/src/main/java/com/chiller3/bcr/NotificationActionService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.chiller3.bcr

import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log

class NotificationActionService : Service() {
companion object {
private val TAG = NotificationActionService::class.java.simpleName

private val ACTION_DELETE_URI = "${NotificationActionService::class.java.canonicalName}.delete_uri"
private const val EXTRA_REDACTED = "redacted"
private const val EXTRA_NOTIFICATION_ID = "notification_id"

private fun intentFromFile(context: Context, file: OutputFile): Intent =
Intent(context, NotificationActionService::class.java).apply {
setDataAndType(file.uri, file.mimeType)
putExtra(EXTRA_REDACTED, file.redacted)
}

fun createDeleteUriIntent(context: Context, file: OutputFile, notificationId: Int): Intent =
intentFromFile(context, file).apply {
action = ACTION_DELETE_URI
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
}

private val handler = Handler(Looper.getMainLooper())

private fun parseFileFromIntent(intent: Intent): OutputFile =
OutputFile(
intent.data!!,
intent.getStringExtra(EXTRA_REDACTED)!!,
intent.type!!,
)

private fun parseDeleteUriIntent(intent: Intent): Pair<OutputFile, Int> {
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
}
}
34 changes: 22 additions & 12 deletions app/src/main/java/com/chiller3/bcr/Notifications.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -231,8 +243,6 @@ class Notifications(
build()
}

/** Send [notification] without overwriting prior alert notifications. */
private fun notify(notification: Notification) {
notificationManager.notify(notificationId, notification)
++notificationId
}
Expand All @@ -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)
}

Expand All @@ -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)
}

Expand Down
23 changes: 22 additions & 1 deletion app/src/main/java/com/chiller3/bcr/OutputFile.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/chiller3/bcr/RecorderThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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!!)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<string name="notification_recording_succeeded">Successfully recorded call</string>
<string name="notification_action_open">Open</string>
<string name="notification_action_share">Share</string>
<string name="notification_action_delete">Delete</string>

<!-- Quick settings tile -->
<string name="quick_settings_label">Call recording</string>
Expand Down

0 comments on commit df042ba

Please sign in to comment.