diff --git a/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt index 8fd2f01a..5d1c55eb 100644 --- a/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt @@ -531,6 +531,7 @@ class MainActivity : SimpleActivity() { .setLongLabel(newEvent) .setIcon(Icon.createWithBitmap(bmp)) .setIntent(intent) + .setRank(0) .build() } diff --git a/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt b/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt index c2b41150..d54e7d1b 100644 --- a/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt @@ -151,6 +151,7 @@ import org.fossify.messages.helpers.CAPTURE_AUDIO_INTENT import org.fossify.messages.helpers.CAPTURE_PHOTO_INTENT import org.fossify.messages.helpers.CAPTURE_VIDEO_INTENT import org.fossify.messages.helpers.FILE_SIZE_NONE +import org.fossify.messages.helpers.IS_LAUNCHED_FROM_SHORTCUT import org.fossify.messages.helpers.IS_RECYCLE_BIN import org.fossify.messages.helpers.MESSAGES_LIMIT import org.fossify.messages.helpers.PICK_CONTACT_INTENT @@ -192,7 +193,6 @@ import org.joda.time.DateTime import java.io.File import java.io.InputStream import java.io.OutputStream -import kotlin.collections.set class ThreadActivity : SimpleActivity() { private val MIN_DATE_TIME_DIFF_SECS = 300 @@ -220,6 +220,7 @@ class ThreadActivity : SimpleActivity() { private var allMessagesFetched = false private var oldestMessageDate = -1 private var isRecycleBin = false + private var isLaunchedFromShortcut = false private var isScheduledMessage: Boolean = false private var messageToResend: Long? = null @@ -263,6 +264,7 @@ class ThreadActivity : SimpleActivity() { binding.threadToolbar.title = it } isRecycleBin = intent.getBooleanExtra(IS_RECYCLE_BIN, false) + isLaunchedFromShortcut = intent.getBooleanExtra(IS_LAUNCHED_FROM_SHORTCUT, false) bus = EventBus.getDefault() bus!!.register(this) @@ -463,6 +465,16 @@ class ThreadActivity : SimpleActivity() { } private fun setupThread() { + if (conversation == null && isLaunchedFromShortcut) { + if (isTaskRoot) { + Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(this) + } + } + finish() + return + } val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) ensureBackgroundThread { privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) diff --git a/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt b/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt index af30e6ba..0ce7dbd1 100644 --- a/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt +++ b/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt @@ -39,7 +39,6 @@ import org.fossify.commons.extensions.normalizeString import org.fossify.commons.extensions.notificationManager import org.fossify.commons.extensions.queryCursor import org.fossify.commons.extensions.showErrorToast -import org.fossify.commons.extensions.toInt import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.trimToComparableNumber import org.fossify.commons.helpers.DAY_SECONDS @@ -59,6 +58,7 @@ import org.fossify.messages.helpers.FILE_SIZE_NONE import org.fossify.messages.helpers.MAX_MESSAGE_LENGTH import org.fossify.messages.helpers.MESSAGES_LIMIT import org.fossify.messages.helpers.NotificationHelper +import org.fossify.messages.helpers.ShortcutHelper import org.fossify.messages.helpers.generateRandomId import org.fossify.messages.interfaces.AttachmentsDao import org.fossify.messages.interfaces.ConversationsDao @@ -107,6 +107,8 @@ val Context.messagingUtils val Context.smsSender get() = SmsSender.getInstance(applicationContext as Application) +val Context.shortcutHelper get() = ShortcutHelper(this) + fun Context.getMessages( threadId: Long, getImageResolutions: Boolean, @@ -855,6 +857,9 @@ fun Context.deleteConversation(threadId: Long) { config.removeCustomNotificationsByThreadId(threadId) notificationManager.deleteNotificationChannel(threadId.toString()) } + if(shortcutHelper.getShortcut(threadId) != null) { + shortcutHelper.removeShortcutForThread(threadId) + } } fun Context.checkAndDeleteOldRecycleBinMessages(callback: (() -> Unit)? = null) { @@ -1253,6 +1258,9 @@ fun Context.renameConversation(conversation: Conversation, newTitle: String): Co val updatedConv = conversation.copy(title = newTitle, usesCustomTitle = true) try { conversationsDB.insertOrUpdate(updatedConv) + ensureBackgroundThread { + shortcutHelper.createOrUpdateShortcut(updatedConv) + } } catch (e: Exception) { e.printStackTrace() } diff --git a/app/src/main/kotlin/org/fossify/messages/extensions/SimpleContact.kt b/app/src/main/kotlin/org/fossify/messages/extensions/SimpleContact.kt index 8a06a4c6..b39cec86 100644 --- a/app/src/main/kotlin/org/fossify/messages/extensions/SimpleContact.kt +++ b/app/src/main/kotlin/org/fossify/messages/extensions/SimpleContact.kt @@ -1,8 +1,52 @@ package org.fossify.messages.extensions +import android.content.Context +import android.graphics.BitmapFactory +import android.net.Uri +import android.provider.ContactsContract import android.text.TextUtils +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import org.fossify.commons.helpers.SimpleContactsHelper import org.fossify.commons.models.SimpleContact +import androidx.core.net.toUri -fun ArrayList.getThreadTitle(): String = TextUtils.join(", ", map { it.name }.toTypedArray()).orEmpty() +fun ArrayList.getThreadTitle(): String { + return TextUtils.join(", ", map { it.name }.toTypedArray()).orEmpty() +} + +fun ArrayList.getAddresses(): List { + return flatMap { it.phoneNumbers }.map { it.normalizedNumber } +} + +fun SimpleContact.toPerson(context: Context? = null): Person { + val uri = + Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, contactId.toString()) + val iconCompat = if (context != null) { + loadIcon(context) + } else { + IconCompat.createWithContentUri(photoUri) + } + + return Person.Builder() + .setName(name) + .setUri(uri.toString()) + .setIcon(iconCompat) + .setKey(uri.toString()) + .build() +} + +fun SimpleContact.loadIcon(context: Context): IconCompat { + try { + val stream = context.contentResolver.openInputStream(photoUri.toUri()) + val bitmap = BitmapFactory.decodeStream(stream) + stream?.close() + val iconCompat = IconCompat.createWithAdaptiveBitmap(bitmap) + return iconCompat + } catch (e: Exception) { + return IconCompat.createWithAdaptiveBitmap( + SimpleContactsHelper(context).getContactLetterIcon(name) + ) + } +} -fun ArrayList.getAddresses() = flatMap { it.phoneNumbers }.map { it.normalizedNumber } diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt index 3e673d1b..3f4e1082 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt @@ -46,6 +46,7 @@ const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check" const val IS_RECYCLE_BIN = "is_recycle_bin" const val IS_ARCHIVE_AVAILABLE = "is_archive_available" const val CUSTOM_NOTIFICATIONS = "custom_notifications" +const val IS_LAUNCHED_FROM_SHORTCUT = "is_launched_from_shortcut" private const val PATH = "org.fossify.org.fossify.messages.action." const val MARK_AS_READ = PATH + "mark_as_read" diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/NotificationHelper.kt b/app/src/main/kotlin/org/fossify/messages/helpers/NotificationHelper.kt index 9b5f207c..e09db9aa 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/NotificationHelper.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/NotificationHelper.kt @@ -17,9 +17,11 @@ import androidx.core.app.RemoteInput import org.fossify.commons.extensions.getProperPrimaryColor import org.fossify.commons.extensions.notificationManager import org.fossify.commons.helpers.SimpleContactsHelper +import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.messages.R import org.fossify.messages.activities.ThreadActivity import org.fossify.messages.extensions.config +import org.fossify.messages.extensions.shortcutHelper import org.fossify.messages.messaging.isShortCodeWithLetters import org.fossify.messages.receivers.DeleteSmsReceiver import org.fossify.messages.receivers.DirectReplyReceiver @@ -166,7 +168,22 @@ class NotificationHelper(private val context: Context) { deleteSmsPendingIntent ).setChannelId(notificationChannelId) } - notificationManager.notify(notificationId, builder.build()) + + var shortcut = context.shortcutHelper.getShortcut(threadId) + if (shortcut == null) { + ensureBackgroundThread { + shortcut = context.shortcutHelper.createOrUpdateShortcut(threadId) + builder.setShortcutInfo(shortcut) + notificationManager.notify(notificationId, builder.build()) + context.shortcutHelper.reportReceiveMessageUsage(threadId) + } + } else { + builder.setShortcutInfo(shortcut) + notificationManager.notify(notificationId, builder.build()) + ensureBackgroundThread { + context.shortcutHelper.reportReceiveMessageUsage(threadId) + } + } } @SuppressLint("NewApi") diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/ShortcutHelper.kt b/app/src/main/kotlin/org/fossify/messages/helpers/ShortcutHelper.kt new file mode 100644 index 00000000..f8c9f681 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/messages/helpers/ShortcutHelper.kt @@ -0,0 +1,170 @@ +package org.fossify.messages.helpers + +import android.content.Context +import android.content.Intent +import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.text.isDigitsOnly +import org.fossify.commons.extensions.getMyContactsCursor +import org.fossify.commons.helpers.MyContactsContentProvider +import org.fossify.commons.helpers.SimpleContactsHelper +import org.fossify.commons.helpers.isOnMainThread +import org.fossify.commons.models.SimpleContact +import org.fossify.messages.activities.ThreadActivity +import org.fossify.messages.extensions.conversationsDB +import org.fossify.messages.extensions.getThreadParticipants +import org.fossify.messages.extensions.toPerson +import org.fossify.messages.models.Conversation + + +class ShortcutHelper(private val context: Context) { + val contactsHelper = SimpleContactsHelper(context) + + fun getShortcuts(): List { + return ShortcutManagerCompat.getDynamicShortcuts(context) + } + + fun getShortcut(threadId: Long): ShortcutInfoCompat? { + return getShortcuts().find { it.id == threadId.toString() } + } + + fun buildShortcut( + conv: Conversation, + capabilities: List = emptyList(), + ): ShortcutInfoCompat { + val contactsMap: HashMap? = if (!isOnMainThread()) { + val privateCursor = + context.getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) + val contacts = MyContactsContentProvider.getSimpleContacts(context, privateCursor) + HashMap(contacts.associateBy { it.rawId }) + } else { + null + } + + val participants = context.getThreadParticipants(conv.threadId, contactsMap) + val persons: Array = participants.map { it.toPerson(context) }.toTypedArray() + val intent = Intent(context, ThreadActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(THREAD_ID, conv.threadId) + putExtra(THREAD_TITLE, conv.title) + putExtra(IS_RECYCLE_BIN, false) // TODO: verify that thread isn't in recycle bin + putExtra(IS_LAUNCHED_FROM_SHORTCUT, true) + putExtra(THREAD_NUMBER, conv.phoneNumber.ifEmpty { "unknown_phone_number" }) + addCategory(Intent.CATEGORY_DEFAULT) + addCategory(Intent.CATEGORY_BROWSABLE) + } + + val shortcut = ShortcutInfoCompat.Builder(context, conv.threadId.toString()).apply { + setShortLabel(conv.title) + setLongLabel(conv.title) + setIsConversation() + setLongLived(true) + setPersons(persons) + setIntent(intent) + setRank(1) + if (!conv.isGroupConversation && !conv.usesCustomTitle) { + setIcon(persons[0].icon) + } else { + val icon = if (conv.isGroupConversation) { + IconCompat.createWithAdaptiveBitmap( + contactsHelper.getColoredGroupIcon(conv.title).toBitmap() + ) + } else { + IconCompat.createWithAdaptiveBitmap( + contactsHelper.getContactLetterIcon(conv.title) + ) + } + setIcon(icon) + } + capabilities.forEach { c -> + addCapabilityBinding(c) + } + if (!shouldPresentShortcut(conv)) { + setRank(99) + } + }.build() + + return shortcut + } + + fun buildShortcut( + threadId: Long, + capabilities: List = emptyList(), + ): ShortcutInfoCompat { + val conv = if (!isOnMainThread()) { + context.conversationsDB.getConversationWithThreadId(threadId) + } else { + null + } + + if (conv == null) { + val conv = Conversation( + threadId = threadId, + snippet = "", + date = 0, + read = false, + title = threadId.toString(), + photoUri = "", + isGroupConversation = false, + phoneNumber = "", + isScheduled = false, + usesCustomTitle = false, + isArchived = false, + ) + return buildShortcut(conv, capabilities) + } + + return buildShortcut(conv, capabilities) + } + + fun createOrUpdateShortcut(conv: Conversation): ShortcutInfoCompat { + val shortcut = buildShortcut(conv) + createOrUpdateShortcut(shortcut) + return shortcut + } + + fun createOrUpdateShortcut(threadId: Long): ShortcutInfoCompat { + val shortcut = buildShortcut(threadId) + createOrUpdateShortcut(shortcut) + return shortcut + } + + private fun createOrUpdateShortcut(shortcut: ShortcutInfoCompat) { + if (getShortcut(shortcut.id.toLong()) != null) { + ShortcutManagerCompat.updateShortcuts(context, listOf(shortcut)) + return + } + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + /** + * Report the usage of a thread. create shortcut if it doesn't exist + */ + fun reportSendMessageUsage(threadId: Long) { + val shortcut = buildShortcut(threadId, listOf("actions.intent.SEND_MESSAGE")) + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + fun reportReceiveMessageUsage(threadId: Long) { + val shortcut = buildShortcut(threadId, listOf("actions.intent.RECEIVE_MESSAGE")) + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + fun removeShortcutForThread(threadId: Long) { + val shortcut = getShortcut(threadId) ?: return + ShortcutManagerCompat.removeDynamicShortcuts(context, mutableListOf(shortcut.id)) + } + + fun shouldPresentShortcut(conv: Conversation): Boolean { + if (conv.isGroupConversation) { + return true + } + if (conv.isArchived || !conv.phoneNumber.isDigitsOnly()) { + return false + } + return true + } +} diff --git a/app/src/main/kotlin/org/fossify/messages/messaging/Messaging.kt b/app/src/main/kotlin/org/fossify/messages/messaging/Messaging.kt index 99ea2ec5..3011fc1d 100644 --- a/app/src/main/kotlin/org/fossify/messages/messaging/Messaging.kt +++ b/app/src/main/kotlin/org/fossify/messages/messaging/Messaging.kt @@ -7,9 +7,12 @@ import android.widget.Toast.LENGTH_LONG import com.klinker.android.send_message.Settings import org.fossify.commons.extensions.showErrorToast import org.fossify.commons.extensions.toast +import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.messages.R import org.fossify.messages.extensions.config +import org.fossify.messages.extensions.getThreadId import org.fossify.messages.extensions.messagingUtils +import org.fossify.messages.extensions.shortcutHelper import org.fossify.messages.messaging.SmsException.Companion.EMPTY_DESTINATION_ADDRESS import org.fossify.messages.messaging.SmsException.Companion.ERROR_PERSISTING_MESSAGE import org.fossify.messages.messaging.SmsException.Companion.ERROR_SENDING_MESSAGE @@ -94,6 +97,10 @@ fun Context.sendMessageCompat( showErrorToast(e) } } + ensureBackgroundThread { + val threadId = getThreadId(addresses.toSet()) + shortcutHelper.reportSendMessageUsage(threadId) + } } /**