diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index da1b2c538..84a763c94 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -40,6 +40,10 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.content.LocusIdCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R @@ -60,6 +64,7 @@ import org.linphone.core.tools.Log import org.linphone.ui.call.CallActivity import org.linphone.utils.AppUtils import org.linphone.utils.LinphoneUtils +import org.linphone.utils.ShortcutUtils class NotificationsManager @MainThread constructor(private val context: Context) { companion object { @@ -81,6 +86,8 @@ class NotificationsManager @MainThread constructor(private val context: Context) const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" } + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val notificationManager: NotificationManagerCompat by lazy { NotificationManagerCompat.from(context) } @@ -145,6 +152,17 @@ class NotificationsManager @MainThread constructor(private val context: Context) return } + if (ShortcutUtils.isShortcutToChatRoomAlreadyCreated(context, chatRoom)) { + Log.i("$TAG Chat room shortcut already exists") + } else { + Log.i( + "$TAG Ensure chat room shortcut exists for bubble notification" + ) + scope.launch { + ShortcutUtils.createShortcutsToChatRooms(context) + } + } + showChatRoomNotification(chatRoom, messages) } @@ -345,6 +363,10 @@ class NotificationsManager @MainThread constructor(private val context: Context) fun onCoreStarted(core: Core) { Log.i("$TAG Core has been started") core.addListener(coreListener) + + scope.launch { + ShortcutUtils.createShortcutsToChatRooms(context) + } } @WorkerThread @@ -801,8 +823,6 @@ class NotificationsManager @MainThread constructor(private val context: Context) notificationBuilder.addPerson(person) } - // TODO FIXME: shortcuts! - return notificationBuilder.build() } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 33f201467..b2892899d 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -21,10 +21,7 @@ package org.linphone.utils import android.annotation.SuppressLint import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.PorterDuff -import android.graphics.Rect import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater @@ -41,7 +38,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.drawable.toBitmap import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnLayout @@ -53,9 +49,7 @@ import androidx.emoji2.emojipicker.EmojiViewItem import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import coil.dispose -import coil.imageLoader import coil.load -import coil.request.ImageRequest import com.google.android.material.imageview.ShapeableImageView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope @@ -356,61 +350,7 @@ private suspend fun loadContactPictureWithCoil( } else { AppUtils.getDimension(R.dimen.avatar_list_cell_size).toInt() } - val bitmap = Bitmap.createBitmap(w, w, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - - val drawables = images.mapNotNull { - val request = ImageRequest.Builder(imageView.context) - .data(it) - .size(w / 2) - .allowHardware(false) - .build() - context.imageLoader.execute(request).drawable - } - - val rectangles = if (drawables.size == 2) { - arrayListOf( - Rect(0, 0, w / 2, w), - Rect(w / 2, 0, w, w) - ) - } else if (drawables.size == 3) { - arrayListOf( - Rect(0, 0, w / 2, w / 2), - Rect(w / 2, 0, w, w / 2), - Rect(0, w / 2, w, w) - ) - } else if (drawables.size >= 4) { - arrayListOf( - Rect(0, 0, w / 2, w / 2), - Rect(w / 2, 0, w, w / 2), - Rect(0, w / 2, w / 2, w), - Rect(w / 2, w / 2, w, w) - ) - } else { - arrayListOf() - } - - for (i in 0 until rectangles.size) { - val src = if (drawables.size == 3 && i == 2) { - // To prevent deformation for the bottom image when merging 3 of them - val quarter = w / 4 - Rect(0, quarter, w, 3 * quarter) - } else if (drawables.size == 2) { - // To prevent deformation when two images are next to each other - val quarter = w / 4 - Rect(quarter, 0, 3 * quarter, w) - } else { - null - } - - canvas.drawBitmap( - drawables[i].toBitmap(w, w, Bitmap.Config.ARGB_8888), - src, - rectangles[i], - null - ) - } - + val bitmap = ImageUtils.getBitmapFromMultipleAvatars(imageView.context, w, images) imageView.load(bitmap) } } else { diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt index ac8d18d4a..debaf9267 100644 --- a/app/src/main/java/org/linphone/utils/ImageUtils.kt +++ b/app/src/main/java/org/linphone/utils/ImageUtils.kt @@ -21,9 +21,15 @@ package org.linphone.utils import android.content.Context import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.ImageDecoder +import android.graphics.Rect import android.net.Uri +import androidx.annotation.AnyThread import androidx.annotation.WorkerThread +import androidx.core.graphics.drawable.toBitmap +import coil.imageLoader +import coil.request.ImageRequest import java.io.FileNotFoundException import org.linphone.core.tools.Log @@ -64,5 +70,65 @@ class ImageUtils { Log.e("$TAG Can't get bitmap from null URI") return null } + + @AnyThread + suspend fun getBitmapFromMultipleAvatars(context: Context, size: Int, images: List): Bitmap { + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val drawables = images.mapNotNull { + val request = ImageRequest.Builder(context) + .data(it) + .size(size / 2) + .allowHardware(false) + .build() + context.imageLoader.execute(request).drawable + } + + val rectangles = if (drawables.size == 2) { + arrayListOf( + Rect(0, 0, size / 2, size), + Rect(size / 2, 0, size, size) + ) + } else if (drawables.size == 3) { + arrayListOf( + Rect(0, 0, size / 2, size / 2), + Rect(size / 2, 0, size, size / 2), + Rect(0, size / 2, size, size) + ) + } else if (drawables.size >= 4) { + arrayListOf( + Rect(0, 0, size / 2, size / 2), + Rect(size / 2, 0, size, size / 2), + Rect(0, size / 2, size / 2, size), + Rect(size / 2, size / 2, size, size) + ) + } else { + arrayListOf() + } + + for (i in 0 until rectangles.size) { + val src = if (drawables.size == 3 && i == 2) { + // To prevent deformation for the bottom image when merging 3 of them + val quarter = size / 4 + Rect(0, quarter, size, 3 * quarter) + } else if (drawables.size == 2) { + // To prevent deformation when two images are next to each other + val quarter = size / 4 + Rect(quarter, 0, 3 * quarter, size) + } else { + null + } + + canvas.drawBitmap( + drawables[i].toBitmap(size, size, Bitmap.Config.ARGB_8888), + src, + rectangles[i], + null + ) + } + + return bitmap + } } } diff --git a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt new file mode 100644 index 000000000..44e5e4a6b --- /dev/null +++ b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.os.Bundle +import androidx.annotation.WorkerThread +import androidx.collection.ArraySet +import androidx.core.app.Person +import androidx.core.content.LocusIdCompat +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import kotlin.math.min +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.contacts.getPerson +import org.linphone.core.ChatRoom +import org.linphone.core.tools.Log +import org.linphone.mediastream.Version +import org.linphone.ui.main.MainActivity + +class ShortcutUtils { + companion object { + private const val TAG = "[Shortcut Utils]" + + @WorkerThread + suspend fun createShortcutsToChatRooms(context: Context) { + val shortcuts = ArrayList() + if (ShortcutManagerCompat.isRateLimitingActive(context)) { + Log.e("$TAG Rate limiting is active, aborting") + return + } + Log.i("$TAG Creating launcher shortcuts for chat rooms") + val maxShortcuts = min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(context), 5) + var count = 0 + for (room in coreContext.core.chatRooms) { + // Android can usually only have around 4-5 shortcuts at a time + if (count >= maxShortcuts) { + Log.w("$TAG Max amount of shortcuts reached ($count)") + break + } + + val shortcut: ShortcutInfoCompat? = createChatRoomShortcut(context, room) + if (shortcut != null) { + Log.i("$TAG Created launcher shortcut for ${shortcut.shortLabel}") + shortcuts.add(shortcut) + count += 1 + } + } + ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts) + Log.i("$TAG Created $count launcher shortcuts") + } + + @WorkerThread + private suspend fun createChatRoomShortcut(context: Context, chatRoom: ChatRoom): ShortcutInfoCompat? { + val localAddress = chatRoom.localAddress + val peerAddress = chatRoom.peerAddress + val id = LinphoneUtils.getChatRoomId(localAddress, peerAddress) + + try { + val categories: ArraySet = ArraySet() + categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) + + val personsList = arrayListOf() + val subject: String + val icon: IconCompat + if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { + val contact = + coreContext.contactsManager.findContactByAddress(peerAddress) + val person = contact?.getPerson() + if (person != null) { + personsList.add(person) + } + + icon = person?.icon ?: coreContext.contactsManager.contactAvatar + subject = contact?.name ?: LinphoneUtils.getDisplayName(peerAddress) + } else if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && chatRoom.participants.isNotEmpty()) { + val address = chatRoom.participants.first().address + val contact = + coreContext.contactsManager.findContactByAddress(address) + val person = contact?.getPerson() + if (person != null) { + personsList.add(person) + } + + subject = contact?.name ?: LinphoneUtils.getDisplayName(address) + icon = person?.icon ?: coreContext.contactsManager.contactAvatar + } else { + val list = arrayListOf() + for (participant in chatRoom.participants) { + val contact = + coreContext.contactsManager.findContactByAddress(participant.address) + if (contact != null) { + personsList.add(contact.getPerson()) + + val picture = contact.photo + if (picture != null) { + list.add(picture) + } + } + } + subject = chatRoom.subject.orEmpty() + val iconSize = AppUtils.getDimension(R.dimen.avatar_list_cell_size).toInt() + icon = IconCompat.createWithAdaptiveBitmap( + ImageUtils.getBitmapFromMultipleAvatars(context, iconSize, list) + ) + } + + val persons = arrayOfNulls(personsList.size) + personsList.toArray(persons) + + val localSipUri = localAddress.asStringUriOnly() + val peerSipUri = peerAddress.asStringUriOnly() + + val args = Bundle() + args.putString("RemoteSipUri", peerSipUri) + args.putString("LocalSipUri", localSipUri) + + val intent = Intent(Intent.ACTION_MAIN) + intent.setClass(context, MainActivity::class.java) + intent.putExtra("Chat", true) + intent.putExtra("RemoteSipUri", peerSipUri) + intent.putExtra("LocalSipUri", localSipUri) + + return ShortcutInfoCompat.Builder(context, id) + .setShortLabel(subject) + .setIcon(icon) + .setPersons(persons) + .setCategories(categories) + .setIntent(intent) + .setLongLived(Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) + .setLocusId(LocusIdCompat(id)) + .build() + } catch (e: Exception) { + Log.e("$TAG createChatRoomShortcut for id [$id] exception: $e") + } + + return null + } + + @WorkerThread + fun isShortcutToChatRoomAlreadyCreated(context: Context, chatRoom: ChatRoom): Boolean { + val id = LinphoneUtils.getChatRoomId(chatRoom) + val found = ShortcutManagerCompat.getDynamicShortcuts(context).find { + it.id == id + } + return found != null + } + } +} diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml index e82f7df3c..513b02d4d 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml index df3a98fea..6ea2e234d 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml index 1d29b6a85..348f7f12f 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml index 6ea2e234d..57fe57f90 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index 9f5e3047b..452bc80cc 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -50,8 +50,8 @@ @@ -72,8 +72,8 @@ android:id="@+id/text_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="26dp" - android:layout_marginTop="12dp" + android:layout_marginStart="20dp" + android:layout_marginTop="@dimen/chat_bubble_text_padding_with_bubble" android:layout_marginEnd="16dp" android:paddingBottom="@{model.groupedWithNextOne ? @dimen/chat_bubble_text_padding_with_status : @dimen/chat_bubble_text_padding_with_bubble, default=@dimen/chat_bubble_text_padding_with_status}" android:text="@{model.text, default=`Lorem ipsum dolor sit amet`}" diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml index 103bcc85b..801807e0d 100644 --- a/app/src/main/res/layout/chat_bubble_outgoing.xml +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -5,6 +5,7 @@ + + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="@{model.isGroupedWithPreviousOne ? @dimen/chat_bubble_grouped_top_margin : @dimen/chat_bubble_top_margin, default=@dimen/chat_bubble_top_margin}"> + + - - - - + app:layout_constraintBottom_toBottomOf="@id/date_time"/> - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + app:layout_constraintEnd_toStartOf="@id/delivery_status"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index ac9639cbd..6589b67c5 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -14,13 +14,13 @@ 24dp 100dp - 24dp + 30dp 45dp 50dp 100dp 120dp - 5dp + 8dp 12dp 26dp 2dp