Merge pull request #353 from FossifyOrg/remove_storage_permissions

Removed storage permission requirement
This commit is contained in:
Naveen Singh 2025-03-24 01:05:25 +05:30 committed by GitHub
commit 9d45c9d81f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 168 additions and 153 deletions

View file

@ -15,10 +15,6 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission <uses-permission
android:name="android.permission.USE_BIOMETRIC" android:name="android.permission.USE_BIOMETRIC"

View file

@ -1,7 +1,6 @@
package org.fossify.messages.activities package org.fossify.messages.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.role.RoleManager import android.app.role.RoleManager
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
@ -208,7 +207,7 @@ class MainActivity : SimpleActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData) super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == MAKE_DEFAULT_APP_REQUEST) { if (requestCode == MAKE_DEFAULT_APP_REQUEST) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == RESULT_OK) {
askPermissions() askPermissions()
} else { } else {
finish() finish()
@ -251,7 +250,8 @@ class MainActivity : SimpleActivity() {
} }
} }
// while SEND_SMS and READ_SMS permissions are mandatory, READ_CONTACTS is optional. If we don't have it, we just won't be able to show the contact name in some cases // while SEND_SMS and READ_SMS permissions are mandatory, READ_CONTACTS is optional.
// If we don't have it, we just won't be able to show the contact name in some cases
private fun askPermissions() { private fun askPermissions() {
handlePermission(PERMISSION_READ_SMS) { handlePermission(PERMISSION_READ_SMS) {
if (it) { if (it) {
@ -271,7 +271,7 @@ class MainActivity : SimpleActivity() {
bus = EventBus.getDefault() bus = EventBus.getDefault()
try { try {
bus!!.register(this) bus!!.register(this)
} catch (ignored: Exception) { } catch (_: Exception) {
} }
} }
} else { } else {
@ -302,20 +302,22 @@ class MainActivity : SimpleActivity() {
ensureBackgroundThread { ensureBackgroundThread {
val conversations = try { val conversations = try {
conversationsDB.getNonArchived().toMutableList() as ArrayList<Conversation> conversationsDB.getNonArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) { } catch (_: Exception) {
ArrayList() ArrayList()
} }
val archived = try { val archived = try {
conversationsDB.getAllArchived() conversationsDB.getAllArchived()
} catch (e: Exception) { } catch (_: Exception) {
listOf() listOf()
} }
updateUnreadCountBadge(conversations) updateUnreadCountBadge(conversations)
runOnUiThread { runOnUiThread {
setupConversations(conversations, cached = true) setupConversations(conversations, cached = true)
getNewConversations((conversations + archived).toMutableList() as ArrayList<Conversation>) getNewConversations(
(conversations + archived).toMutableList() as ArrayList<Conversation>
)
} }
conversations.forEach { conversations.forEach {
clearExpiredScheduledMessages(it.threadId) clearExpiredScheduledMessages(it.threadId)
@ -349,11 +351,14 @@ class MainActivity : SimpleActivity() {
val newConversation = val newConversation =
conversations.find { it.phoneNumber == cachedConversation.phoneNumber } conversations.find { it.phoneNumber == cachedConversation.phoneNumber }
if (isTemporaryThread && newConversation != null) { if (isTemporaryThread && newConversation != null) {
// delete the original temporary thread and move any scheduled messages to the new thread // delete the original temporary thread and move any scheduled messages
// to the new thread
conversationsDB.deleteThreadId(threadId) conversationsDB.deleteThreadId(threadId)
messagesDB.getScheduledThreadMessages(threadId) messagesDB.getScheduledThreadMessages(threadId)
.forEach { message -> .forEach { message ->
messagesDB.insertOrUpdate(message.copy(threadId = newConversation.threadId)) messagesDB.insertOrUpdate(
message.copy(threadId = newConversation.threadId)
)
} }
insertOrUpdateConversation(newConversation, cachedConversation) insertOrUpdateConversation(newConversation, cachedConversation)
} }
@ -366,7 +371,8 @@ class MainActivity : SimpleActivity() {
) )
} }
if (conv != null) { if (conv != null) {
// FIXME: Scheduled message date is being reset here. Conversations with scheduled messages will have their original date. // FIXME: Scheduled message date is being reset here. Conversations with
// scheduled messages will have their original date.
insertOrUpdateConversation(conv) insertOrUpdateConversation(conv)
} }
} }
@ -412,15 +418,18 @@ class MainActivity : SimpleActivity() {
private fun setupConversations( private fun setupConversations(
conversations: ArrayList<Conversation>, conversations: ArrayList<Conversation>,
cached: Boolean = false cached: Boolean = false,
) { ) {
val sortedConversations = conversations.sortedWith( val sortedConversations = conversations
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) } .sortedWith(
.thenByDescending { it.date } compareByDescending<Conversation> {
).toMutableList() as ArrayList<Conversation> config.pinnedConversations.contains(it.threadId.toString())
}.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
if (cached && config.appRunCount == 1) { if (cached && config.appRunCount == 1) {
// there are no cached conversations on the first run so we show the loading placeholder and progress until we are done loading from telephony // there are no cached conversations on the first run so we show the
// loading placeholder and progress until we are done loading from telephony
showOrHideProgress(conversations.isEmpty()) showOrHideProgress(conversations.isEmpty())
} else { } else {
showOrHideProgress(false) showOrHideProgress(false)
@ -435,7 +444,7 @@ class MainActivity : SimpleActivity() {
} }
} }
} }
} catch (ignored: Exception) { } catch (_: Exception) {
} }
} }
@ -498,7 +507,7 @@ class MainActivity : SimpleActivity() {
try { try {
manager.dynamicShortcuts = listOf(newConversation) manager.dynamicShortcuts = listOf(newConversation)
config.lastHandledShortcutColor = appIconColor config.lastHandledShortcutColor = appIconColor
} catch (ignored: Exception) { } catch (_: Exception) {
} }
} }
} }

View file

@ -5,24 +5,17 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import org.fossify.commons.dialogs.ExportBlockedNumbersDialog
import org.fossify.commons.dialogs.FilePickerDialog
import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.beVisibleIf
import org.fossify.commons.extensions.getFileOutputStream
import org.fossify.commons.extensions.getProperPrimaryColor import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.getTempFile import org.fossify.commons.extensions.getTempFile
import org.fossify.commons.extensions.showErrorToast import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.extensions.toFileDirItem
import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.toast
import org.fossify.commons.extensions.underlineText import org.fossify.commons.extensions.underlineText
import org.fossify.commons.extensions.updateTextColors import org.fossify.commons.extensions.updateTextColors
import org.fossify.commons.extensions.viewBinding import org.fossify.commons.extensions.viewBinding
import org.fossify.commons.helpers.ExportResult import org.fossify.commons.helpers.ExportResult
import org.fossify.commons.helpers.NavigationIcon import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.PERMISSION_READ_STORAGE
import org.fossify.commons.helpers.PERMISSION_WRITE_STORAGE
import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.helpers.isQPlus
import org.fossify.commons.interfaces.RefreshRecyclerViewListener import org.fossify.commons.interfaces.RefreshRecyclerViewListener
import org.fossify.messages.R import org.fossify.messages.R
import org.fossify.messages.databinding.ActivityManageBlockedKeywordsBinding import org.fossify.messages.databinding.ActivityManageBlockedKeywordsBinding
@ -96,7 +89,7 @@ class ManageBlockedKeywordsActivity : SimpleActivity(), RefreshRecyclerViewListe
} }
} }
private val exportActivityResultLauncher = private val createDocument =
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
try { try {
val outputStream = uri?.let { contentResolver.openOutputStream(it) } val outputStream = uri?.let { contentResolver.openOutputStream(it) }
@ -108,7 +101,7 @@ class ManageBlockedKeywordsActivity : SimpleActivity(), RefreshRecyclerViewListe
} }
} }
private val importActivityResultLauncher = private val getContent =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
try { try {
if (uri != null) { if (uri != null) {
@ -120,27 +113,13 @@ class ManageBlockedKeywordsActivity : SimpleActivity(), RefreshRecyclerViewListe
} }
private fun tryImportBlockedKeywords() { private fun tryImportBlockedKeywords() {
if (isQPlus()) { val mimeType = "text/plain"
val mimeType = "text/plain" try {
try { getContent.launch(mimeType)
importActivityResultLauncher.launch(mimeType) } catch (_: ActivityNotFoundException) {
} catch (e: ActivityNotFoundException) { toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG)
toast(org.fossify.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) } catch (e: Exception) {
} catch (e: Exception) { showErrorToast(e)
showErrorToast(e)
}
} else {
handlePermission(PERMISSION_READ_STORAGE) { isAllowed ->
if (isAllowed) {
pickFileToImportBlockedKeywords()
}
}
}
}
private fun pickFileToImportBlockedKeywords() {
FilePickerDialog(this) {
importBlockedKeywords(it)
} }
} }
@ -200,32 +179,20 @@ class ManageBlockedKeywordsActivity : SimpleActivity(), RefreshRecyclerViewListe
} }
private fun tryExportBlockedNumbers() { private fun tryExportBlockedNumbers() {
if (isQPlus()) { ExportBlockedKeywordsDialog(
ExportBlockedKeywordsDialog(this, config.lastBlockedKeywordExportPath, true) { file -> activity = this,
try { path = config.lastBlockedKeywordExportPath,
exportActivityResultLauncher.launch(file.name) hidePath = true
} catch (e: ActivityNotFoundException) { ) { file ->
toast( try {
org.fossify.commons.R.string.system_service_disabled, createDocument.launch(file.name)
Toast.LENGTH_LONG } catch (_: ActivityNotFoundException) {
) toast(
} catch (e: Exception) { org.fossify.commons.R.string.system_service_disabled,
showErrorToast(e) Toast.LENGTH_LONG
} )
} } catch (e: Exception) {
} else { showErrorToast(e)
handlePermission(PERMISSION_WRITE_STORAGE) { isAllowed ->
if (isAllowed) {
ExportBlockedNumbersDialog(
this,
config.lastBlockedKeywordExportPath,
false
) { file ->
getFileOutputStream(file.toFileDirItem(this), true) { out ->
exportBlockedKeywordsTo(out)
}
}
}
} }
} }
} }

View file

@ -89,8 +89,9 @@ abstract class BaseConversationsAdapter(
override fun getSelectableItemCount() = itemCount override fun getSelectableItemCount() = itemCount
protected fun getSelectedItems() = protected fun getSelectedItems() = currentList.filter {
currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> selectedKeys.contains(it.hashCode())
} as ArrayList<Conversation>
override fun getIsItemSelectable(position: Int) = true override fun getIsItemSelectable(position: Int) = true
@ -143,7 +144,9 @@ abstract class BaseConversationsAdapter(
draftIndicator.beVisibleIf(!smsDraft.isNullOrEmpty()) draftIndicator.beVisibleIf(!smsDraft.isNullOrEmpty())
draftIndicator.setTextColor(properPrimaryColor) draftIndicator.setTextColor(properPrimaryColor)
pinIndicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString())) pinIndicator.beVisibleIf(
activity.config.pinnedConversations.contains(conversation.threadId.toString())
)
pinIndicator.applyColorFilter(textColor) pinIndicator.applyColorFilter(textColor)
conversationFrame.isSelected = selectedKeys.contains(conversation.hashCode()) conversationFrame.isSelected = selectedKeys.contains(conversation.hashCode())

View file

@ -14,6 +14,7 @@ import org.fossify.commons.extensions.setupDialogStuff
import org.fossify.commons.extensions.showErrorToast import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.toast
import org.fossify.commons.extensions.value import org.fossify.commons.extensions.value
import org.fossify.commons.helpers.MEDIUM_ALPHA
import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.R import org.fossify.messages.R
import org.fossify.messages.activities.SimpleActivity import org.fossify.messages.activities.SimpleActivity
@ -72,7 +73,7 @@ class ExportMessagesDialog(
getButton(AlertDialog.BUTTON_NEGATIVE) getButton(AlertDialog.BUTTON_NEGATIVE)
).forEach { ).forEach {
it.isEnabled = false it.isEnabled = false
it.alpha = 0.6f it.alpha = MEDIUM_ALPHA
} }
binding.exportProgress.setIndicatorColor(activity.getProperPrimaryColor()) binding.exportProgress.setIndicatorColor(activity.getProperPrimaryColor())
@ -112,7 +113,7 @@ class ExportMessagesDialog(
// delete the file to avoid leaving behind an empty/corrupt file // delete the file to avoid leaving behind an empty/corrupt file
try { try {
DocumentsContract.deleteDocument(activity.contentResolver, uri) DocumentsContract.deleteDocument(activity.contentResolver, uri)
} catch (ignored: Exception) { } catch (_: Exception) {
// ignored because we don't want to show two error messages // ignored because we don't want to show two error messages
} }
} }

View file

@ -5,6 +5,7 @@ import org.fossify.commons.extensions.getAlertDialogBuilder
import org.fossify.commons.extensions.getProperPrimaryColor import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.setupDialogStuff import org.fossify.commons.extensions.setupDialogStuff
import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.toast
import org.fossify.commons.helpers.MEDIUM_ALPHA
import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.R import org.fossify.messages.R
import org.fossify.messages.activities.SimpleActivity import org.fossify.messages.activities.SimpleActivity
@ -65,7 +66,7 @@ class ImportMessagesDialog(
negativeButton negativeButton
).forEach { ).forEach {
it.isEnabled = false it.isEnabled = false
it.alpha = 0.6f it.alpha = MEDIUM_ALPHA
} }
ensureBackgroundThread { ensureBackgroundThread {

View file

@ -29,7 +29,7 @@ fun Activity.dialNumber(phoneNumber: String, callback: (() -> Unit)? = null) {
try { try {
startActivity(this) startActivity(this)
callback?.invoke() callback?.invoke()
} catch (e: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
toast(org.fossify.commons.R.string.no_app_found) toast(org.fossify.commons.R.string.no_app_found)
} catch (e: Exception) { } catch (e: Exception) {
showErrorToast(e) showErrorToast(e)
@ -46,7 +46,7 @@ fun Activity.launchViewIntent(uri: Uri, mimetype: String, filename: String) {
try { try {
hideKeyboard() hideKeyboard()
startActivity(this) startActivity(this)
} catch (e: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
val newMimetype = filename.getMimeType() val newMimetype = filename.getMimeType()
if (newMimetype.isNotEmpty() && mimetype != newMimetype) { if (newMimetype.isNotEmpty() && mimetype != newMimetype) {
launchViewIntent(uri, newMimetype, filename) launchViewIntent(uri, newMimetype, filename)

View file

@ -78,32 +78,41 @@ import org.fossify.messages.models.RecycleBinMessage
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.FileNotFoundException import java.io.FileNotFoundException
val Context.config: Config get() = Config.newInstance(applicationContext) val Context.config: Config
get() = Config.newInstance(applicationContext)
fun Context.getMessagesDB() = MessagesDatabase.getInstance(this) fun Context.getMessagesDB() = MessagesDatabase.getInstance(this)
val Context.conversationsDB: ConversationsDao get() = getMessagesDB().ConversationsDao() val Context.conversationsDB: ConversationsDao
get() = getMessagesDB().ConversationsDao()
val Context.attachmentsDB: AttachmentsDao get() = getMessagesDB().AttachmentsDao() val Context.attachmentsDB: AttachmentsDao
get() = getMessagesDB().AttachmentsDao()
val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB().MessageAttachmentsDao() val Context.messageAttachmentsDB: MessageAttachmentsDao
get() = getMessagesDB().MessageAttachmentsDao()
val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao() val Context.messagesDB: MessagesDao
get() = getMessagesDB().MessagesDao()
val Context.draftsDB: DraftsDao get() = getMessagesDB().DraftsDao() val Context.draftsDB: DraftsDao
get() = getMessagesDB().DraftsDao()
val Context.notificationHelper get() = NotificationHelper(this) val Context.notificationHelper
get() = NotificationHelper(this)
val Context.messagingUtils get() = MessagingUtils(this) val Context.messagingUtils
get() = MessagingUtils(this)
val Context.smsSender get() = SmsSender.getInstance(applicationContext as Application) val Context.smsSender
get() = SmsSender.getInstance(applicationContext as Application)
fun Context.getMessages( fun Context.getMessages(
threadId: Long, threadId: Long,
getImageResolutions: Boolean, getImageResolutions: Boolean,
dateFrom: Int = -1, dateFrom: Int = -1,
includeScheduledMessages: Boolean = true, includeScheduledMessages: Boolean = true,
limit: Int = MESSAGES_LIMIT limit: Int = MESSAGES_LIMIT,
): ArrayList<Message> { ): ArrayList<Message> {
val uri = Sms.CONTENT_URI val uri = Sms.CONTENT_URI
val projection = arrayOf( val projection = arrayOf(
@ -212,7 +221,7 @@ fun Context.getMMS(
threadId: Long? = null, threadId: Long? = null,
getImageResolutions: Boolean = false, getImageResolutions: Boolean = false,
sortOrder: String? = null, sortOrder: String? = null,
dateFrom: Int = -1 dateFrom: Int = -1,
): ArrayList<Message> { ): ArrayList<Message> {
val uri = Mms.CONTENT_URI val uri = Mms.CONTENT_URI
val projection = arrayOf( val projection = arrayOf(
@ -229,8 +238,8 @@ fun Context.getMMS(
var selectionArgs: Array<String>? = null var selectionArgs: Array<String>? = null
if (threadId == null && dateFrom != -1) { if (threadId == null && dateFrom != -1) {
selection = // Should not multiply 1000 here, because date in mms's database is different from sms's.
"${Sms.DATE} < ${dateFrom.toLong()}" //Should not multiply 1000 here, because date in mms's database is different from sms's. selection = "${Sms.DATE} < ${dateFrom.toLong()}"
} else if (threadId != null && dateFrom == -1) { } else if (threadId != null && dateFrom == -1) {
selection = "${Sms.THREAD_ID} = ?" selection = "${Sms.THREAD_ID} = ?"
selectionArgs = arrayOf(threadId.toString()) selectionArgs = arrayOf(threadId.toString())
@ -315,18 +324,18 @@ fun Context.getMMSSender(msgId: Long): String {
return it.getStringValue(Mms.Addr.ADDRESS) return it.getStringValue(Mms.Addr.ADDRESS)
} }
} }
} catch (ignored: Exception) { } catch (_: Exception) {
} }
return "" return ""
} }
fun Context.getConversations( fun Context.getConversations(
threadId: Long? = null, threadId: Long? = null,
privateContacts: ArrayList<SimpleContact> = ArrayList() privateContacts: ArrayList<SimpleContact> = ArrayList(),
): ArrayList<Conversation> { ): ArrayList<Conversation> {
val archiveAvailable = config.isArchiveAvailable val archiveAvailable = config.isArchiveAvailable
val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true") val uri = "${Threads.CONTENT_URI}?simple=true".toUri()
val projection = mutableListOf( val projection = mutableListOf(
Threads._ID, Threads._ID,
Threads.SNIPPET, Threads.SNIPPET,
@ -413,7 +422,10 @@ fun Context.getConversations(
conversations.add(conversation) conversations.add(conversation)
} }
} catch (sqliteException: SQLiteException) { } catch (sqliteException: SQLiteException) {
if (sqliteException.message?.contains("no such column: archived") == true && archiveAvailable) { if (
sqliteException.message?.contains("no such column: archived") == true
&& archiveAvailable
) {
config.isArchiveAvailable = false config.isArchiveAvailable = false
return getConversations(threadId, privateContacts) return getConversations(threadId, privateContacts)
} else { } else {
@ -433,7 +445,7 @@ private fun Context.queryCursorUnsafe(
selection: String? = null, selection: String? = null,
selectionArgs: Array<String>? = null, selectionArgs: Array<String>? = null,
sortOrder: String? = null, sortOrder: String? = null,
callback: (cursor: Cursor) -> Unit callback: (cursor: Cursor) -> Unit,
) { ) {
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder) val cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)
cursor?.use { cursor?.use {
@ -446,7 +458,7 @@ private fun Context.queryCursorUnsafe(
} }
fun Context.getConversationIds(): List<Long> { fun Context.getConversationIds(): List<Long> {
val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true") val uri = "${Threads.CONTENT_URI}?simple=true".toUri()
val projection = arrayOf(Threads._ID) val projection = arrayOf(Threads._ID)
val selection = "${Threads.MESSAGE_COUNT} > 0" val selection = "${Threads.MESSAGE_COUNT} > 0"
val sortOrder = "${Threads.DATE} ASC" val sortOrder = "${Threads.DATE} ASC"
@ -464,7 +476,7 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
val uri = if (isQPlus()) { val uri = if (isQPlus()) {
Mms.Part.CONTENT_URI Mms.Part.CONTENT_URI
} else { } else {
Uri.parse("content://mms/part") "content://mms/part".toUri()
} }
val projection = arrayOf( val projection = arrayOf(
@ -502,12 +514,21 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
) )
width = options.outWidth width = options.outWidth
height = options.outHeight height = options.outHeight
} catch (e: Exception) { } catch (_: Exception) {
} }
} }
val attachment = Attachment(partId, id, fileUri.toString(), mimetype, width, height, "") messageAttachment.attachments.add(
messageAttachment.attachments.add(attachment) Attachment(
id = partId,
messageId = id,
uriString = fileUri.toString(),
mimetype = mimetype,
width = width,
height = height,
filename = ""
)
)
} else if (mimetype != "application/smil") { } else if (mimetype != "application/smil") {
val attachmentName = attachmentNames?.getOrNull(attachmentCount) ?: "" val attachmentName = attachmentNames?.getOrNull(attachmentCount) ?: ""
val attachment = Attachment( val attachment = Attachment(
@ -562,7 +583,7 @@ fun Context.getThreadSnippet(threadId: Long): String {
snippet = cursor.getStringValue(Sms.BODY) snippet = cursor.getStringValue(Sms.BODY)
} }
} }
} catch (ignored: Exception) { } catch (_: Exception) {
} }
return snippet return snippet
} }
@ -583,7 +604,7 @@ fun Context.getMessageRecipientAddress(messageId: Long): String {
return cursor.getStringValue(Sms.ADDRESS) return cursor.getStringValue(Sms.ADDRESS)
} }
} }
} catch (e: Exception) { } catch (_: Exception) {
} }
return "" return ""
@ -591,9 +612,9 @@ fun Context.getMessageRecipientAddress(messageId: Long): String {
fun Context.getThreadParticipants( fun Context.getThreadParticipants(
threadId: Long, threadId: Long,
contactsMap: HashMap<Int, SimpleContact>? contactsMap: HashMap<Int, SimpleContact>?,
): ArrayList<SimpleContact> { ): ArrayList<SimpleContact> {
val uri = Uri.parse("${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true") val uri = "${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true".toUri()
val projection = arrayOf( val projection = arrayOf(
ThreadsColumns.RECIPIENT_IDS ThreadsColumns.RECIPIENT_IDS
) )
@ -646,7 +667,7 @@ fun Context.getThreadPhoneNumbers(recipientIds: List<Int>): ArrayList<String> {
fun Context.getThreadContactNames( fun Context.getThreadContactNames(
phoneNumbers: List<String>, phoneNumbers: List<String>,
privateContacts: ArrayList<SimpleContact> privateContacts: ArrayList<SimpleContact>,
): ArrayList<String> { ): ArrayList<String> {
val names = ArrayList<String>() val names = ArrayList<String>()
phoneNumbers.forEach { number -> phoneNumbers.forEach { number ->
@ -686,7 +707,9 @@ fun Context.getPhoneNumberFromAddressId(canonicalAddressId: Int): String {
return "" return ""
} }
fun Context.getSuggestedContacts(privateContacts: ArrayList<SimpleContact>): ArrayList<SimpleContact> { fun Context.getSuggestedContacts(
privateContacts: ArrayList<SimpleContact>,
): ArrayList<SimpleContact> {
val contacts = ArrayList<SimpleContact>() val contacts = ArrayList<SimpleContact>()
val uri = Sms.CONTENT_URI val uri = Sms.CONTENT_URI
val projection = arrayOf( val projection = arrayOf(
@ -705,8 +728,9 @@ fun Context.getSuggestedContacts(privateContacts: ArrayList<SimpleContact>): Arr
return@queryCursor return@queryCursor
} else if (namePhoto.name == senderNumber) { } else if (namePhoto.name == senderNumber) {
if (privateContacts.isNotEmpty()) { if (privateContacts.isNotEmpty()) {
val privateContact = val privateContact = privateContacts.firstOrNull {
privateContacts.firstOrNull { it.phoneNumbers.first().normalizedNumber == senderNumber } it.phoneNumbers.first().normalizedNumber == senderNumber
}
if (privateContact != null) { if (privateContact != null) {
senderName = privateContact.name senderName = privateContact.name
photoUri = privateContact.photoUri photoUri = privateContact.photoUri
@ -757,7 +781,7 @@ fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
return NamePhoto(name, photoUri) return NamePhoto(name, photoUri)
} }
} }
} catch (e: Exception) { } catch (_: Exception) {
} }
return NamePhoto(number, null) return NamePhoto(number, null)
@ -771,7 +795,7 @@ fun Context.insertNewSMS(
read: Int, read: Int,
threadId: Long, threadId: Long,
type: Int, type: Int,
subscriptionId: Int subscriptionId: Int,
): Long { ): Long {
val uri = Sms.CONTENT_URI val uri = Sms.CONTENT_URI
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
@ -788,7 +812,7 @@ fun Context.insertNewSMS(
return try { return try {
val newUri = contentResolver.insert(uri, contentValues) val newUri = contentResolver.insert(uri, contentValues)
newUri?.lastPathSegment?.toLong() ?: 0L newUri?.lastPathSegment?.toLong() ?: 0L
} catch (e: Exception) { } catch (_: Exception) {
0L 0L
} }
} }
@ -801,7 +825,7 @@ fun Context.removeAllArchivedConversations(callback: (() -> Unit)? = null) {
} }
toast(R.string.archive_emptied_successfully) toast(R.string.archive_emptied_successfully)
callback?.invoke() callback?.invoke()
} catch (e: Exception) { } catch (_: Exception) {
toast(org.fossify.commons.R.string.unknown_error_occurred) toast(org.fossify.commons.R.string.unknown_error_occurred)
} }
} }
@ -834,15 +858,20 @@ fun Context.deleteConversation(threadId: Long) {
} }
fun Context.checkAndDeleteOldRecycleBinMessages(callback: (() -> Unit)? = null) { fun Context.checkAndDeleteOldRecycleBinMessages(callback: (() -> Unit)? = null) {
if (config.useRecycleBin && config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) { if (
config.useRecycleBin
&& config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000
) {
config.lastRecycleBinCheck = System.currentTimeMillis() config.lastRecycleBinCheck = System.currentTimeMillis()
ensureBackgroundThread { ensureBackgroundThread {
try { try {
for (message in messagesDB.getOldRecycleBinMessages(System.currentTimeMillis() - MONTH_SECONDS * 1000L)) { messagesDB.getOldRecycleBinMessages(
timestamp = System.currentTimeMillis() - MONTH_SECONDS * 1000L
).forEach { message ->
deleteMessage(message.id, message.isMMS) deleteMessage(message.id, message.isMMS)
} }
callback?.invoke() callback?.invoke()
} catch (e: Exception) { } catch (_: Exception) {
} }
} }
} }
@ -892,7 +921,10 @@ fun Context.updateConversationArchivedStatus(threadId: Long, archived: Boolean)
try { try {
contentResolver.update(uri, values, selection, selectionArgs) contentResolver.update(uri, values, selection, selectionArgs)
} catch (sqliteException: SQLiteException) { } catch (sqliteException: SQLiteException) {
if (sqliteException.message?.contains("no such column: archived") == true && config.isArchiveAvailable) { if (
sqliteException.message?.contains("no such column: archived") == true
&& config.isArchiveAvailable
) {
config.isArchiveAvailable = false config.isArchiveAvailable = false
return return
} else { } else {
@ -976,7 +1008,7 @@ fun Context.updateUnreadCountBadge(conversations: List<Conversation>) {
fun Context.getThreadId(address: String): Long { fun Context.getThreadId(address: String): Long {
return try { return try {
Threads.getOrCreateThreadId(this, address) Threads.getOrCreateThreadId(this, address)
} catch (e: Exception) { } catch (_: Exception) {
0L 0L
} }
} }
@ -985,7 +1017,7 @@ fun Context.getThreadId(address: String): Long {
fun Context.getThreadId(addresses: Set<String>): Long { fun Context.getThreadId(addresses: Set<String>): Long {
return try { return try {
Threads.getOrCreateThreadId(this, addresses) Threads.getOrCreateThreadId(this, addresses)
} catch (e: Exception) { } catch (_: Exception) {
0L 0L
} }
} }
@ -995,7 +1027,7 @@ fun Context.showReceivedMessageNotification(
address: String, address: String,
body: String, body: String,
threadId: Long, threadId: Long,
bitmap: Bitmap? bitmap: Bitmap?,
) { ) {
val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread { ensureBackgroundThread {
@ -1055,7 +1087,7 @@ fun Context.getNotificationBitmap(photoUri: String): Bitmap? {
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.into(size, size) .into(size, size)
.get() .get()
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
@ -1067,7 +1099,7 @@ fun Context.removeDiacriticsIfNeeded(text: String): String {
fun Context.getSmsDraft(threadId: Long): String { fun Context.getSmsDraft(threadId: Long): String {
val draft = try { val draft = try {
draftsDB.getDraftById(threadId) draftsDB.getDraftById(threadId)
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
@ -1145,14 +1177,14 @@ fun Context.updateLastConversationMessage(threadIds: Iterable<Long>) {
val newConversation = getConversations(threadId)[0] val newConversation = getConversations(threadId)[0]
insertOrUpdateConversation(newConversation) insertOrUpdateConversation(newConversation)
} }
} catch (e: Exception) { } catch (_: Exception) {
} }
} }
fun Context.getFileSizeFromUri(uri: Uri): Long { fun Context.getFileSizeFromUri(uri: Uri): Long {
val assetFileDescriptor = try { val assetFileDescriptor = try {
contentResolver.openAssetFileDescriptor(uri, "r") contentResolver.openAssetFileDescriptor(uri, "r")
} catch (e: FileNotFoundException) { } catch (_: FileNotFoundException) {
null null
} }
@ -1205,12 +1237,14 @@ fun Context.subscriptionManagerCompat(): SubscriptionManager {
fun Context.insertOrUpdateConversation( fun Context.insertOrUpdateConversation(
conversation: Conversation, conversation: Conversation,
cachedConv: Conversation? = conversationsDB.getConversationWithThreadId(conversation.threadId) cachedConv: Conversation? = conversationsDB.getConversationWithThreadId(conversation.threadId),
) { ) {
var updatedConv = conversation var updatedConv = conversation
if (cachedConv != null && cachedConv.usesCustomTitle) { if (cachedConv != null && cachedConv.usesCustomTitle) {
updatedConv = updatedConv = updatedConv.copy(
updatedConv.copy(title = cachedConv.title, usesCustomTitle = cachedConv.usesCustomTitle) title = cachedConv.title,
usesCustomTitle = true
)
} }
conversationsDB.insertOrUpdate(updatedConv) conversationsDB.insertOrUpdate(updatedConv)
} }
@ -1228,12 +1262,16 @@ fun Context.renameConversation(conversation: Conversation, newTitle: String): Co
fun Context.createTemporaryThread( fun Context.createTemporaryThread(
message: Message, message: Message,
threadId: Long = generateRandomId(), threadId: Long = generateRandomId(),
cachedConv: Conversation? cachedConv: Conversation?,
) { ) {
val simpleContactHelper = SimpleContactsHelper(this) val simpleContactHelper = SimpleContactsHelper(this)
val addresses = message.participants.getAddresses() val addresses = message.participants.getAddresses()
val photoUri = val photoUri = if (addresses.size == 1) {
if (addresses.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(addresses.first()) else "" simpleContactHelper.getPhotoUriFromPhoneNumber(addresses.first())
} else {
""
}
val title = if (cachedConv != null && cachedConv.usesCustomTitle) { val title = if (cachedConv != null && cachedConv.usesCustomTitle) {
cachedConv.title cachedConv.title
} else { } else {
@ -1286,5 +1324,6 @@ fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List
} }
} }
fun Context.getDefaultKeyboardHeight() = fun Context.getDefaultKeyboardHeight(): Int {
resources.getDimensionPixelSize(R.dimen.default_keyboard_height) return resources.getDimensionPixelSize(R.dimen.default_keyboard_height)
}

View file

@ -59,7 +59,8 @@ class MessagesImporter(private val activity: SimpleActivity) {
val messages = if (isUpsideDownCakePlus()) { val messages = if (isUpsideDownCakePlus()) {
deserializedList.map { message -> deserializedList.map { message ->
// workaround for messages not being imported on Android 14 when the device has a different subscriptionId (see #191) // workaround for messages not being imported on Android 14 when the device
// has a different subscriptionId (see #191)
when (message) { when (message) {
is SmsBackup -> message.copy(subscriptionId = -1) is SmsBackup -> message.copy(subscriptionId = -1)
is MmsBackup -> message.copy(subscriptionId = -1) is MmsBackup -> message.copy(subscriptionId = -1)
@ -70,9 +71,9 @@ class MessagesImporter(private val activity: SimpleActivity) {
} }
ImportMessagesDialog(activity, messages) ImportMessagesDialog(activity, messages)
} catch (e: SerializationException) { } catch (_: SerializationException) {
activity.toast(org.fossify.commons.R.string.invalid_file_format) activity.toast(org.fossify.commons.R.string.invalid_file_format)
} catch (e: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
activity.toast(org.fossify.commons.R.string.invalid_file_format) activity.toast(org.fossify.commons.R.string.invalid_file_format)
} catch (e: Exception) { } catch (e: Exception) {
activity.showErrorToast(e) activity.showErrorToast(e)
@ -201,7 +202,7 @@ class MessagesImporter(private val activity: SimpleActivity) {
private fun getInputStreamFromUri(uri: Uri): InputStream? { private fun getInputStreamFromUri(uri: Uri): InputStream? {
return try { return try {
activity.contentResolver.openInputStream(uri) activity.contentResolver.openInputStream(uri)
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
@ -209,14 +210,12 @@ class MessagesImporter(private val activity: SimpleActivity) {
private fun isFileXml(uri: Uri): Boolean { private fun isFileXml(uri: Uri): Boolean {
val inputStream = getInputStreamFromUri(uri) val inputStream = getInputStreamFromUri(uri)
return inputStream?.bufferedReader()?.use { reader -> return inputStream?.bufferedReader()?.use { reader ->
reader.readLine()?.startsWith("<?xml") ?: false reader.readLine()?.startsWith("<?xml") == true
} ?: false } == true
} }
private fun isXmlMimeType(mimeType: String): Boolean { private fun isXmlMimeType(mimeType: String): Boolean {
return mimeType.equals("application/xml", ignoreCase = true) || mimeType.equals( return mimeType.equals("application/xml", ignoreCase = true)
other = "text/xml", || mimeType.equals(other = "text/xml", ignoreCase = true)
ignoreCase = true
)
} }
} }

View file

@ -15,7 +15,7 @@ eventbus = "3.3.1"
#Room #Room
room = "2.6.1" room = "2.6.1"
#Fossify #Fossify
commons = "29e9b0d13e" commons = "79b1077e39"
android-smsmms = "c3e678befd" android-smsmms = "c3e678befd"
indicator-fast-scroll = "4524cd0b61" indicator-fast-scroll = "4524cd0b61"
#Gradle #Gradle