Merge pull request #6746 from thundernest/contact_permission
Message details: Hide "Add to contacts" button when contacts permission is missing
This commit is contained in:
commit
cbea30e5b8
12 changed files with 127 additions and 46 deletions
|
@ -11,6 +11,7 @@ val messageDetailsUiModule = module {
|
|||
folderRepository = get(),
|
||||
contactSettingsProvider = get(),
|
||||
contactRepository = get(),
|
||||
contactPermissionResolver = get(),
|
||||
clipboardManager = get(),
|
||||
accountManager = get(),
|
||||
participantFormatter = get(),
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package com.fsck.k9.ui.messagedetails
|
||||
|
||||
data class MessageDetailsAppearance(
|
||||
val showContactPicture: Boolean,
|
||||
val alwaysHideAddToContactsButton: Boolean,
|
||||
)
|
|
@ -121,13 +121,17 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
progressBar.isVisible = false
|
||||
errorView.isVisible = false
|
||||
recyclerView.isVisible = true
|
||||
setMessageDetails(recyclerView, state.details, state.showContactPicture)
|
||||
setMessageDetails(recyclerView, state.details, state.appearance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMessageDetails(recyclerView: RecyclerView, details: MessageDetailsUi, showContactPicture: Boolean) {
|
||||
private fun setMessageDetails(
|
||||
recyclerView: RecyclerView,
|
||||
details: MessageDetailsUi,
|
||||
appearance: MessageDetailsAppearance,
|
||||
) {
|
||||
val itemAdapter = ItemAdapter<GenericItem>().apply {
|
||||
add(MessageDateItem(details.date ?: getString(R.string.message_details_missing_date)))
|
||||
|
||||
|
@ -135,15 +139,15 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
add(CryptoStatusItem(details.cryptoDetails))
|
||||
}
|
||||
|
||||
addParticipants(details.from, R.string.message_details_from_section_title, showContactPicture)
|
||||
addParticipants(details.sender, R.string.message_details_sender_section_title, showContactPicture)
|
||||
addParticipants(details.replyTo, R.string.message_details_replyto_section_title, showContactPicture)
|
||||
addParticipants(details.from, R.string.message_details_from_section_title, appearance)
|
||||
addParticipants(details.sender, R.string.message_details_sender_section_title, appearance)
|
||||
addParticipants(details.replyTo, R.string.message_details_replyto_section_title, appearance)
|
||||
|
||||
add(MessageDetailsDividerItem())
|
||||
|
||||
addParticipants(details.to, R.string.message_details_to_section_title, showContactPicture)
|
||||
addParticipants(details.cc, R.string.message_details_cc_section_title, showContactPicture)
|
||||
addParticipants(details.bcc, R.string.message_details_bcc_section_title, showContactPicture)
|
||||
addParticipants(details.to, R.string.message_details_to_section_title, appearance)
|
||||
addParticipants(details.cc, R.string.message_details_cc_section_title, appearance)
|
||||
addParticipants(details.bcc, R.string.message_details_bcc_section_title, appearance)
|
||||
|
||||
if (details.folder != null) {
|
||||
addFolderName(details.folder)
|
||||
|
@ -163,14 +167,21 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
private fun ItemAdapter<GenericItem>.addParticipants(
|
||||
participants: List<Participant>,
|
||||
@StringRes title: Int,
|
||||
showContactPicture: Boolean,
|
||||
appearance: MessageDetailsAppearance,
|
||||
) {
|
||||
if (participants.isNotEmpty()) {
|
||||
val extraText = if (participants.size > 1) participants.size.toString() else null
|
||||
add(SectionHeaderItem(title = getString(title), extra = extraText))
|
||||
|
||||
for (participant in participants) {
|
||||
add(ParticipantItem(contactPictureLoader, showContactPicture, participant))
|
||||
add(
|
||||
ParticipantItem(
|
||||
contactPictureLoader,
|
||||
appearance.showContactPicture,
|
||||
appearance.alwaysHideAddToContactsButton,
|
||||
participant,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.PendingIntent
|
|||
import android.content.res.Resources
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.android.common.contact.ContactPermissionResolver
|
||||
import app.k9mail.core.android.common.contact.ContactRepository
|
||||
import app.k9mail.core.common.mail.EmailAddress
|
||||
import com.fsck.k9.Account
|
||||
|
@ -34,6 +35,7 @@ internal class MessageDetailsViewModel(
|
|||
private val folderRepository: FolderRepository,
|
||||
private val contactSettingsProvider: ContactSettingsProvider,
|
||||
private val contactRepository: ContactRepository,
|
||||
private val contactPermissionResolver: ContactPermissionResolver,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val accountManager: AccountManager,
|
||||
private val participantFormatter: MessageDetailsParticipantFormatter,
|
||||
|
@ -67,8 +69,13 @@ internal class MessageDetailsViewModel(
|
|||
folder = folder?.toFolderInfo(),
|
||||
)
|
||||
|
||||
MessageDetailsState.DataLoaded(
|
||||
val messageDetailsAppearance = MessageDetailsAppearance(
|
||||
showContactPicture = contactSettingsProvider.isShowContactPicture,
|
||||
alwaysHideAddToContactsButton = !contactPermissionResolver.hasContactPermission(),
|
||||
)
|
||||
|
||||
MessageDetailsState.DataLoaded(
|
||||
appearance = messageDetailsAppearance,
|
||||
details = messageDetailsUi,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
@ -155,7 +162,7 @@ sealed interface MessageDetailsState {
|
|||
object Loading : MessageDetailsState
|
||||
object Error : MessageDetailsState
|
||||
data class DataLoaded(
|
||||
val showContactPicture: Boolean,
|
||||
val appearance: MessageDetailsAppearance,
|
||||
val details: MessageDetailsUi,
|
||||
) : MessageDetailsState
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.mikepenz.fastadapter.items.AbstractItem
|
|||
internal class ParticipantItem(
|
||||
private val contactPictureLoader: ContactPictureLoader,
|
||||
private val showContactsPicture: Boolean,
|
||||
private val alwaysHideAddContactsButton: Boolean,
|
||||
val participant: Participant,
|
||||
) : AbstractItem<ParticipantItem.ViewHolder>() {
|
||||
override val type: Int = R.id.message_details_participant
|
||||
|
@ -44,7 +45,7 @@ internal class ParticipantItem(
|
|||
}
|
||||
email.text = participant.emailAddress
|
||||
|
||||
menuAddContact.isVisible = !participant.isInContacts
|
||||
menuAddContact.isVisible = !item.alwaysHideAddContactsButton && !participant.isInContacts
|
||||
|
||||
if (item.showContactsPicture) {
|
||||
item.contactPictureLoader.setContactPicture(contactPicture, participant.address)
|
||||
|
|
|
@ -174,7 +174,7 @@
|
|||
<ID>LongParameterList:ImapSync.kt$ImapSync$( syncConfig: SyncConfig, remoteFolder: ImapFolder, unsyncedMessages: List<ImapMessage>, smallMessages: MutableList<ImapMessage>, largeMessages: MutableList<ImapMessage>, progress: AtomicInteger, todo: Int, listener: SyncListener, )</ID>
|
||||
<ID>LongParameterList:MessageDatabaseHelpers.kt$( folderId: Long, deleted: Boolean = false, uid: String? = null, subject: String = "", date: Long = 0L, flags: String = "", senderList: String = "", toList: String = "", ccList: String = "", bccList: String = "", replyToList: String = "", attachmentCount: Int = 0, internalDate: Long = 0L, messageIdHeader: String? = null, previewType: DatabasePreviewType = DatabasePreviewType.NONE, preview: String = "", mimeType: String = "text/plain", normalizedSubjectHash: Long = 0L, empty: Boolean = false, read: Boolean = false, flagged: Boolean = false, answered: Boolean = false, forwarded: Boolean = false, messagePartId: Long = 0L, encryptionType: String? = null, newMessage: Boolean = false, )</ID>
|
||||
<ID>LongParameterList:MessageDatabaseHelpers.kt$( type: Int = 0, root: Int? = null, parent: Int = -1, seq: Int = 0, mimeType: String = "text/plain", decodedBodySize: Int = 0, displayName: String? = null, header: String? = null, encoding: String = "7bit", charset: String? = null, dataLocation: Int = 0, data: ByteArray? = null, preamble: String? = null, epilogue: String? = null, boundary: String? = null, contentId: String? = null, serverExtra: String? = null, directory: File? = null, )</ID>
|
||||
<ID>LongParameterList:MessageDetailsViewModel.kt$MessageDetailsViewModel$( private val resources: Resources, private val messageRepository: MessageRepository, private val folderRepository: FolderRepository, private val contactSettingsProvider: ContactSettingsProvider, private val contactRepository: ContactRepository, private val clipboardManager: ClipboardManager, private val accountManager: AccountManager, private val participantFormatter: MessageDetailsParticipantFormatter, private val folderNameFormatter: FolderNameFormatter, )</ID>
|
||||
<ID>LongParameterList:MessageDetailsViewModel.kt$MessageDetailsViewModel$( private val resources: Resources, private val messageRepository: MessageRepository, private val folderRepository: FolderRepository, private val contactSettingsProvider: ContactSettingsProvider, private val contactRepository: ContactRepository, private val contactPermissionResolver: ContactPermissionResolver, private val clipboardManager: ClipboardManager, private val accountManager: AccountManager, private val participantFormatter: MessageDetailsParticipantFormatter, private val folderNameFormatter: FolderNameFormatter, )</ID>
|
||||
<ID>LongParameterList:MessageListAdapterTest.kt$MessageListAdapterTest$( account: Account = Account(SOME_ACCOUNT_UUID), subject: String? = "irrelevant", threadCount: Int = 0, messageDate: Long = 0L, internalDate: Long = 0L, displayName: CharSequence = "irrelevant", displayAddress: Address? = Address.parse("irrelevant@domain.example").first(), previewText: String = "irrelevant", isMessageEncrypted: Boolean = false, isRead: Boolean = false, isStarred: Boolean = false, isAnswered: Boolean = false, isForwarded: Boolean = false, hasAttachments: Boolean = false, uniqueId: Long = 0L, folderId: Long = 0L, messageUid: String = "irrelevant", databaseId: Long = 0L, threadRoot: Long = 0L, )</ID>
|
||||
<ID>LongParameterList:MessageListAdapterTest.kt$MessageListAdapterTest$( fontSizes: FontSizes = createFontSizes(), previewLines: Int = 0, stars: Boolean = true, senderAboveSubject: Boolean = false, showContactPicture: Boolean = true, showingThreadedList: Boolean = true, backGroundAsReadIndicator: Boolean = false, showAccountChip: Boolean = false, density: UiDensity = UiDensity.Default, )</ID>
|
||||
<ID>LongParameterList:MessagePartDatabaseHelpers.kt$( type: Int = MessagePartType.UNKNOWN, root: Long? = null, parent: Long = -1, seq: Int = 0, mimeType: String? = null, decodedBodySize: Int? = null, displayName: String? = null, header: String? = null, encoding: String? = null, charset: String? = null, dataLocation: Int = DataLocation.MISSING, data: ByteArray? = null, preamble: String? = null, epilogue: String? = null, boundary: String? = null, contentId: String? = null, serverExtra: String? = null, )</ID>
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.k9mail.core.android.common.database.EmptyCursor
|
||||
import app.k9mail.core.android.common.database.getLongOrThrow
|
||||
import app.k9mail.core.android.common.database.getStringOrNull
|
||||
|
@ -21,8 +17,8 @@ interface ContactDataSource {
|
|||
}
|
||||
|
||||
internal class ContentResolverContactDataSource(
|
||||
private val context: Context,
|
||||
private val contentResolver: ContentResolver = context.contentResolver,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val contactPermissionResolver: ContactPermissionResolver,
|
||||
) : ContactDataSource {
|
||||
|
||||
override fun getContactFor(emailAddress: EmailAddress): Contact? {
|
||||
|
@ -57,12 +53,12 @@ internal class ContentResolverContactDataSource(
|
|||
}
|
||||
|
||||
private fun getCursorFor(emailAddress: EmailAddress): Cursor {
|
||||
val uri = Uri.withAppendedPath(
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
|
||||
Uri.encode(emailAddress.address),
|
||||
)
|
||||
return if (contactPermissionResolver.hasContactPermission()) {
|
||||
val uri = Uri.withAppendedPath(
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI,
|
||||
Uri.encode(emailAddress.address),
|
||||
)
|
||||
|
||||
return if (hasContactPermission()) {
|
||||
contentResolver.query(
|
||||
uri,
|
||||
PROJECTION,
|
||||
|
@ -75,13 +71,6 @@ internal class ContentResolverContactDataSource(
|
|||
}
|
||||
}
|
||||
|
||||
private fun hasContactPermission(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val SORT_ORDER = ContactsContract.Contacts.DISPLAY_NAME +
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
import android.content.Context
|
||||
import app.k9mail.core.common.cache.Cache
|
||||
import app.k9mail.core.common.cache.ExpiringCache
|
||||
import app.k9mail.core.common.cache.SynchronizedCache
|
||||
|
@ -14,7 +15,10 @@ internal val contactModule = module {
|
|||
)
|
||||
}
|
||||
factory<ContactDataSource> {
|
||||
ContentResolverContactDataSource(context = get())
|
||||
ContentResolverContactDataSource(
|
||||
contentResolver = get<Context>().contentResolver,
|
||||
contactPermissionResolver = get(),
|
||||
)
|
||||
}
|
||||
factory<ContactRepository> {
|
||||
CachingContactRepository(
|
||||
|
@ -22,6 +26,9 @@ internal val contactModule = module {
|
|||
dataSource = get(),
|
||||
)
|
||||
}
|
||||
factory<ContactPermissionResolver> {
|
||||
AndroidContactPermissionResolver(context = get())
|
||||
}
|
||||
}
|
||||
|
||||
internal const val CACHE_NAME = "ContactCache"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
import android.Manifest.permission.READ_CONTACTS
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
interface ContactPermissionResolver {
|
||||
fun hasContactPermission(): Boolean
|
||||
}
|
||||
|
||||
internal class AndroidContactPermissionResolver(private val context: Context) : ContactPermissionResolver {
|
||||
override fun hasContactPermission(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(context, READ_CONTACTS) == PERMISSION_GRANTED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
import android.Manifest
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AndroidContactPermissionResolverTest {
|
||||
private val application = RuntimeEnvironment.getApplication()
|
||||
private val testSubject = AndroidContactPermissionResolver(context = application)
|
||||
|
||||
@Test
|
||||
fun `hasPermission() with contact permission`() {
|
||||
grantContactPermission()
|
||||
|
||||
val result = testSubject.hasContactPermission()
|
||||
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasPermission() without contact permission`() {
|
||||
denyContactPermission()
|
||||
|
||||
val result = testSubject.hasContactPermission()
|
||||
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
private fun grantContactPermission() {
|
||||
Shadows.shadowOf(application).grantPermissions(Manifest.permission.READ_CONTACTS)
|
||||
}
|
||||
|
||||
private fun denyContactPermission() {
|
||||
Shadows.shadowOf(application).denyPermissions(Manifest.permission.READ_CONTACTS)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
|
@ -12,7 +11,6 @@ import assertk.assertions.isFalse
|
|||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import kotlin.test.Test
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
|
@ -20,27 +18,20 @@ import org.mockito.kotlin.eq
|
|||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
internal class ContentResolverContactDataSourceTest {
|
||||
|
||||
private val contactPermissionResolver = TestContactPermissionResolver(hasPermission = true)
|
||||
private val contentResolver = mock<ContentResolver>()
|
||||
|
||||
private val testSubject = ContentResolverContactDataSource(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
contentResolver = contentResolver,
|
||||
contactPermissionResolver = contactPermissionResolver,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Shadows.shadowOf(RuntimeEnvironment.getApplication()).grantPermissions(Manifest.permission.READ_CONTACTS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getContactForEmail() returns null if permission is not granted`() {
|
||||
Shadows.shadowOf(RuntimeEnvironment.getApplication()).denyPermissions(Manifest.permission.READ_CONTACTS)
|
||||
contactPermissionResolver.hasContactPermission = false
|
||||
|
||||
val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS)
|
||||
|
||||
|
@ -67,7 +58,7 @@ internal class ContentResolverContactDataSourceTest {
|
|||
|
||||
@Test
|
||||
fun `hasContactForEmail() returns false if permission is not granted`() {
|
||||
Shadows.shadowOf(RuntimeEnvironment.getApplication()).denyPermissions(Manifest.permission.READ_CONTACTS)
|
||||
contactPermissionResolver.hasContactPermission = false
|
||||
|
||||
val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS)
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.core.android.common.contact
|
||||
|
||||
class TestContactPermissionResolver(hasPermission: Boolean) : ContactPermissionResolver {
|
||||
var hasContactPermission = hasPermission
|
||||
|
||||
override fun hasContactPermission(): Boolean {
|
||||
return hasContactPermission
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue