diff --git a/SETUP.md b/SETUP.md
index 540fca71af6..14aab913bdf 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -106,6 +106,19 @@ This requires a working Internet connection.
The generated APK file is saved in ```app/build/outputs/apk``` as ```app-generic-debug.apk```.
+### Working with Android Auto
+
+To test [notification extension to Android Auto](https://developer.android.com/training/cars/communication/notification-messaging), Developer settings and Unknown sources need to be enabled in
+the Android Auto settings:
+
+1. Open the Settings app on your device
+2. Search for Android Auto, or click on Connected Devices > Android Auto
+3. Scroll all the way down to Version, and click it 10 times to enable Developer settings
+4. Click the 3 dots in the top right and select Developer settings
+5. Enable Unknown sources
+
+You can now receive notifications on Android Auto from a Nextcloud Talk development build.
+
### App flavours
The app is currently equipped to be built with three flavours:
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 54182da11ba..5260266902f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -100,7 +100,11 @@
+ android:value="10"/>
+
+
-
-
-
-
-
+
+
+
+
+
+ newStyle.addMessage(
+ NotificationCompat.MessagingStyle.Message(
+ message.text,
+ message.timestamp,
+ message.person
)
+ )
+ }
+
+ val message = NotificationCompat.MessagingStyle.Message(
+ pushMessage.text,
+ pushMessage.timestamp,
+ sender
+ )
+ if (imageUri != null) {
+ message.setData(imageMimeType ?: "image/*", imageUri)
+ }
+ newStyle.addMessage(message)
+ notificationBuilder.setStyle(newStyle)
+ }
+
+ private fun loadSenderAvatar(notificationUser: NotificationUser?): Bitmap? {
+ val userType = notificationUser?.type
+ if (userType != "user" && userType != "guest") return null
+
+ val baseUrl = user.baseUrl
+ val avatarUrl = if ("user" == userType) {
+ ApiUtils.getUrlForAvatar(
+ baseUrl!!,
+ notificationUser.id,
+ false,
+ darkMode = DisplayUtils.isDarkModeOn(context!!)
+ )
+ } else {
+ ApiUtils.getUrlForGuestAvatar(baseUrl!!, notificationUser.name, false)
}
+ return NotificationUtils.loadAvatarBitmapSync(avatarUrl, context!!)
}
private fun loadImageBitmapSync(imageUrl: String): Bitmap? {
@@ -839,42 +913,43 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
return bitmap
}
- private fun styleChatNotification(
- notificationBuilder: NotificationCompat.Builder,
- activeStatusBarNotification: StatusBarNotification?
- ) {
+ private fun pushConversationShortcut(notificationBuilder: NotificationCompat.Builder, avatarBitmap: Bitmap?) {
val notificationUser = pushMessage.notificationUser ?: return
+ val roomToken = pushMessage.id ?: return
- val userType = notificationUser.type
- var style: NotificationCompat.MessagingStyle? = null
- if (activeStatusBarNotification != null) {
- style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
- activeStatusBarNotification.notification
- )
- }
- val person = Person.Builder()
+ val shortcutId = "conversation_${user.id}_$roomToken"
+
+ val personBuilder = Person.Builder()
.setKey(user.id.toString() + "@" + notificationUser.id)
.setName(EmojiCompat.get().process(notificationUser.name!!))
- .setBot("bot" == userType)
- if ("user" == userType || "guest" == userType) {
- val baseUrl = user.baseUrl
- val avatarUrl = if ("user" == userType) {
- ApiUtils.getUrlForAvatar(
- baseUrl!!,
- notificationUser.id,
- false,
- darkMode = DisplayUtils.isDarkModeOn(context!!)
- )
- } else {
- ApiUtils.getUrlForGuestAvatar(baseUrl!!, notificationUser.name, false)
- }
- person.setIcon(loadAvatarSync(avatarUrl, context!!))
+ if (avatarBitmap != null) {
+ personBuilder.setIcon(IconCompat.createWithBitmap(avatarBitmap))
+ }
+
+ val intent = Intent(context, MainActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ putExtra(KEY_ROOM_TOKEN, roomToken)
+ putExtra(KEY_INTERNAL_USER_ID, user.id)
}
- notificationBuilder.setStyle(getStyle(person.build(), style))
+
+ val shortcut = ShortcutInfoCompat.Builder(context!!, shortcutId)
+ .setShortLabel(pushMessage.subject.ifEmpty { notificationUser.name ?: "Chat" })
+ .setLongLived(true)
+ .setIntent(intent)
+ .setPerson(personBuilder.build())
+ .build()
+
+ ShortcutManagerCompat.pushDynamicShortcut(context!!, shortcut)
+ notificationBuilder.setShortcutId(shortcutId)
}
- private fun buildIntentForAction(cls: Class<*>, systemNotificationId: Int, messageId: Int): PendingIntent {
+ private fun buildIntentForAction(
+ cls: Class<*>,
+ systemNotificationId: Int,
+ messageId: Int,
+ mutable: Boolean = false
+ ): PendingIntent {
val actualIntent = Intent(context, cls)
// NOTE - systemNotificationId is an internal ID used on the device only.
@@ -885,7 +960,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
actualIntent.putExtra(KEY_MESSAGE_ID, messageId)
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ val mutabilityFlag = if (mutable) PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_IMMUTABLE
+ mutabilityFlag or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
@@ -904,7 +980,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val pendingIntent = buildIntentForAction(
MarkAsReadReceiver::class.java,
systemNotificationId,
- messageId
+ messageId,
+ mutable = false
)
val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.ic_mark_chat_read_24px,
@@ -927,7 +1004,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val replyPendingIntent = buildIntentForAction(
DirectReplyReceiver::class.java,
systemNotificationId,
- 0
+ 0,
+ mutable = true
)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
@@ -958,7 +1036,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
dismissIntent.putExtra(KEY_DISMISS_RECORDING_URL, dismissRecordingUrl)
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
@@ -992,7 +1070,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
shareRecordingIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
@@ -1014,25 +1092,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
notificationBuilder.addAction(shareRecordingAction)
}
- private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle {
- val newStyle = NotificationCompat.MessagingStyle(person)
- newStyle.conversationTitle = pushMessage.subject
- newStyle.isGroupConversation = "one2one" != conversationType
- style?.messages?.forEach(
- Consumer { message: NotificationCompat.MessagingStyle.Message ->
- newStyle.addMessage(
- NotificationCompat.MessagingStyle.Message(
- message.text,
- message.timestamp,
- message.person
- )
- )
- }
- )
- newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person)
- return newStyle
- }
-
@Throws(NumberFormatException::class)
private fun parseMessageId(objectId: String): Int {
val objectIdParts = objectId.split("/".toRegex()).toTypedArray()
@@ -1210,7 +1269,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
// See https://github.com/nextcloud/talk-android/issues/2111
val requestCode = System.currentTimeMillis().toInt()
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
index 4023b8083eb..a5a19fa41f9 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
@@ -11,12 +11,14 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
+import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.media.AudioAttributes
import android.net.Uri
import android.service.notification.StatusBarNotification
import android.text.TextUtils
import android.util.Log
+import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import coil.executeBlocking
@@ -30,6 +32,8 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.RingtoneSettings
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
+import java.io.File
+import java.io.FileOutputStream
import java.io.IOException
@Suppress("TooManyFunctions")
@@ -55,6 +59,8 @@ object NotificationUtils {
const val KEY_UPLOAD_GROUP = "com.nextcloud.talk.utils.KEY_UPLOAD_GROUP"
const val GROUP_SUMMARY_NOTIFICATION_ID = -1
+ private const val BITMAP_COMPRESSION_QUALITY = 100
+
private fun createNotificationChannel(
context: Context,
notificationChannel: Channel,
@@ -316,7 +322,12 @@ object NotificationUtils {
)
fun loadAvatarSync(avatarUrl: String, context: Context): IconCompat? {
- var avatarIcon: IconCompat? = null
+ val bitmap = loadAvatarBitmapSync(avatarUrl, context)
+ return bitmap?.let { IconCompat.createWithBitmap(it) }
+ }
+
+ fun loadAvatarBitmapSync(avatarUrl: String, context: Context): Bitmap? {
+ var avatarBitmap: Bitmap? = null
val request = ImageRequest.Builder(context)
.data(avatarUrl)
@@ -324,13 +335,11 @@ object NotificationUtils {
.placeholder(R.drawable.account_circle_96dp)
.target(
onSuccess = { result ->
- val bitmap = (result as BitmapDrawable).bitmap
- avatarIcon = IconCompat.createWithBitmap(bitmap)
+ avatarBitmap = (result as BitmapDrawable).bitmap
},
onError = { error ->
error?.let {
- val bitmap = (error as BitmapDrawable).bitmap
- avatarIcon = IconCompat.createWithBitmap(bitmap)
+ avatarBitmap = (error as BitmapDrawable).bitmap
}
Log.w(TAG, "Can't load avatar for URL: $avatarUrl")
}
@@ -339,7 +348,20 @@ object NotificationUtils {
context.imageLoader.executeBlocking(request)
- return avatarIcon
+ return avatarBitmap
+ }
+
+ fun saveBitmapToCache(context: Context, bitmap: Bitmap, fileName: String): Uri? {
+ val cacheFile = File(context.cacheDir, fileName)
+ return try {
+ FileOutputStream(cacheFile).use { out ->
+ bitmap.compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESSION_QUALITY, out)
+ }
+ FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, cacheFile)
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to save bitmap to cache", e)
+ null
+ }
}
private data class Channel(val id: String, val name: String, val description: String, val isImportant: Boolean)
diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml
new file mode 100644
index 00000000000..3082aa4c1ed
--- /dev/null
+++ b/app/src/main/res/xml/automotive_app_desc.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file