Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new notification action for deleting the recording #182

Merged
merged 1 commit into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add a new notification action for deleting the recording
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 <chillermillerlong@hotmail.com>
  • Loading branch information
chenxiaolong committed Nov 28, 2022
commit df042ba3b5fd2295b1c62db6326ca4463d4af977
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