Rename package to org.fossify.messages

This commit is contained in:
Naveen 2024-01-18 01:05:03 +05:30
parent d71db351ca
commit e2f83f49da
No known key found for this signature in database
GPG key ID: 0E155DAD31671DA3
106 changed files with 417 additions and 418 deletions

View file

@ -0,0 +1,177 @@
package org.fossify.messages.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import org.fossify.commons.dialogs.ConfirmationDialog
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.WAS_PROTECTION_HANDLED
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.R
import org.fossify.messages.adapters.ArchivedConversationsAdapter
import org.fossify.messages.databinding.ActivityArchivedConversationsBinding
import org.fossify.messages.extensions.config
import org.fossify.messages.extensions.conversationsDB
import org.fossify.messages.extensions.removeAllArchivedConversations
import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.helpers.THREAD_TITLE
import org.fossify.messages.models.Conversation
import org.fossify.messages.models.Events
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ArchivedConversationsActivity : SimpleActivity() {
private var bus: EventBus? = null
private val binding by viewBinding(ActivityArchivedConversationsBinding::inflate)
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupOptionsMenu()
updateMaterialActivityViews(
mainCoordinatorLayout = binding.archiveCoordinator,
nestedView = binding.conversationsList,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.conversationsList, toolbar = binding.archiveToolbar)
loadArchivedConversations()
}
override fun onResume() {
super.onResume()
setupToolbar(binding.archiveToolbar, NavigationIcon.Arrow)
updateMenuColors()
loadArchivedConversations()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
private fun setupOptionsMenu() {
binding.archiveToolbar.inflateMenu(R.menu.archive_menu)
binding.archiveToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.empty_archive -> removeAll()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun updateOptionsMenu(conversations: ArrayList<Conversation>) {
binding.archiveToolbar.menu.apply {
findItem(R.id.empty_archive).isVisible = conversations.isNotEmpty()
}
}
private fun updateMenuColors() {
updateStatusbarColor(getProperBackgroundColor())
}
private fun loadArchivedConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getAllArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
runOnUiThread {
setupConversations(conversations)
}
}
bus = EventBus.getDefault()
try {
bus!!.register(this)
} catch (ignored: Exception) {
}
}
private fun removeAll() {
ConfirmationDialog(
activity = this,
message = "",
messageId = R.string.empty_archive_confirmation,
positive = org.fossify.commons.R.string.yes,
negative = org.fossify.commons.R.string.no
) {
removeAllArchivedConversations {
loadArchivedConversations()
}
}
}
private fun getOrCreateConversationsAdapter(): ArchivedConversationsAdapter {
var currAdapter = binding.conversationsList.adapter
if (currAdapter == null) {
hideKeyboard()
currAdapter = ArchivedConversationsAdapter(
activity = this,
recyclerView = binding.conversationsList,
onRefresh = { notifyDatasetChanged() },
itemClick = { handleConversationClick(it) }
)
binding.conversationsList.adapter = currAdapter
if (areSystemAnimationsEnabled) {
binding.conversationsList.scheduleLayoutAnimation()
}
}
return currAdapter as ArchivedConversationsAdapter
}
private fun setupConversations(conversations: ArrayList<Conversation>) {
val sortedConversations = conversations.sortedWith(
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) }
.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
showOrHidePlaceholder(conversations.isEmpty())
updateOptionsMenu(conversations)
try {
getOrCreateConversationsAdapter().apply {
updateConversations(sortedConversations)
}
} catch (ignored: Exception) {
}
}
private fun showOrHidePlaceholder(show: Boolean) {
binding.conversationsFastscroller.beGoneIf(show)
binding.noConversationsPlaceholder.beVisibleIf(show)
binding.noConversationsPlaceholder.setTextColor(getProperTextColor())
binding.noConversationsPlaceholder.text = getString(R.string.no_archived_conversations)
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyDatasetChanged() {
getOrCreateConversationsAdapter().notifyDataSetChanged()
}
private fun handleConversationClick(any: Any) {
Intent(this, ThreadActivity::class.java).apply {
val conversation = any as Conversation
putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
putExtra(WAS_PROTECTION_HANDLED, true)
startActivity(this)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
loadArchivedConversations()
}
}

View file

@ -0,0 +1,161 @@
package org.fossify.messages.activities
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.RingtoneManager
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.content.res.ResourcesCompat
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.helpers.isOreoPlus
import org.fossify.commons.models.SimpleContact
import org.fossify.messages.adapters.ContactsAdapter
import org.fossify.messages.databinding.ActivityConversationDetailsBinding
import org.fossify.messages.dialogs.RenameConversationDialog
import org.fossify.messages.extensions.*
import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.models.Conversation
class ConversationDetailsActivity : SimpleActivity() {
private var threadId: Long = 0L
private var conversation: Conversation? = null
private lateinit var participants: ArrayList<SimpleContact>
private val binding by viewBinding(ActivityConversationDetailsBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
updateMaterialActivityViews(
mainCoordinatorLayout = binding.conversationDetailsCoordinator,
nestedView = binding.participantsRecyclerview,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.participantsRecyclerview, toolbar = binding.conversationDetailsToolbar)
threadId = intent.getLongExtra(THREAD_ID, 0L)
ensureBackgroundThread {
conversation = conversationsDB.getConversationWithThreadId(threadId)
participants = if (conversation != null && conversation!!.isScheduled) {
val message = messagesDB.getThreadMessages(conversation!!.threadId).firstOrNull()
message?.participants ?: arrayListOf()
} else {
getThreadParticipants(threadId, null)
}
runOnUiThread {
setupTextViews()
setupParticipants()
if (isOreoPlus()) {
setupCustomNotifications()
}
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(binding.conversationDetailsToolbar, NavigationIcon.Arrow)
updateTextColors(binding.conversationDetailsHolder)
val primaryColor = getProperPrimaryColor()
binding.conversationNameHeading.setTextColor(primaryColor)
binding.membersHeading.setTextColor(primaryColor)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun setupCustomNotifications() {
binding.apply {
notificationsHeading.beVisible()
customNotificationsHolder.beVisible()
customNotifications.isChecked = config.customNotifications.contains(threadId.toString())
customNotificationsButton.beVisibleIf(customNotifications.isChecked)
customNotificationsHolder.setOnClickListener {
customNotifications.toggle()
if (customNotifications.isChecked) {
customNotificationsButton.beVisible()
config.addCustomNotificationsByThreadId(threadId)
createNotificationChannel()
} else {
customNotificationsButton.beGone()
config.removeCustomNotificationsByThreadId(threadId)
removeNotificationChannel()
}
}
customNotificationsButton.setOnClickListener {
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, threadId.toString())
startActivity(this)
}
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val name = conversation?.title
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION)
.build()
NotificationChannel(threadId.toString(), name, NotificationManager.IMPORTANCE_HIGH).apply {
setBypassDnd(false)
enableLights(true)
setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), audioAttributes)
enableVibration(true)
notificationManager.createNotificationChannel(this)
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun removeNotificationChannel() {
notificationManager.deleteNotificationChannel(threadId.toString())
}
private fun setupTextViews() {
binding.conversationName.apply {
ResourcesCompat.getDrawable(resources, org.fossify.commons.R.drawable.ic_edit_vector, theme)?.apply {
applyColorFilter(getProperTextColor())
setCompoundDrawablesWithIntrinsicBounds(null, null, this, null)
}
text = conversation?.title
setOnClickListener {
RenameConversationDialog(this@ConversationDetailsActivity, conversation!!) { title ->
text = title
ensureBackgroundThread {
conversation = renameConversation(conversation!!, newTitle = title)
}
}
}
}
}
private fun setupParticipants() {
val adapter = ContactsAdapter(this, participants, binding.participantsRecyclerview) {
val contact = it as SimpleContact
val address = contact.phoneNumbers.first().normalizedNumber
getContactFromAddress(address) { simpleContact ->
if (simpleContact != null) {
startContactDetailsIntent(simpleContact)
}
}
}
binding.participantsRecyclerview.adapter = adapter
}
}

View file

@ -0,0 +1,607 @@
package org.fossify.messages.activities
import android.annotation.SuppressLint
import android.app.Activity
import android.app.role.RoleManager
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.provider.Telephony
import android.text.TextUtils
import androidx.coordinatorlayout.widget.CoordinatorLayout
import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.*
import org.fossify.commons.models.FAQItem
import org.fossify.commons.models.Release
import org.fossify.messages.BuildConfig
import org.fossify.messages.R
import org.fossify.messages.adapters.ConversationsAdapter
import org.fossify.messages.adapters.SearchResultsAdapter
import org.fossify.messages.databinding.ActivityMainBinding
import org.fossify.messages.extensions.*
import org.fossify.messages.helpers.SEARCHED_MESSAGE_ID
import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.helpers.THREAD_TITLE
import org.fossify.messages.models.Conversation
import org.fossify.messages.models.Events
import org.fossify.messages.models.Message
import org.fossify.messages.models.SearchResult
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1
private var storedTextColor = 0
private var storedFontSize = 0
private var lastSearchedText = ""
private var bus: EventBus? = null
private var wasProtectionHandled = false
private val binding by viewBinding(ActivityMainBinding::inflate)
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
appLaunched(BuildConfig.APPLICATION_ID)
setupOptionsMenu()
refreshMenuItems()
updateMaterialActivityViews(
mainCoordinatorLayout = binding.mainCoordinator,
nestedView = binding.conversationsList,
useTransparentNavigation = true,
useTopSearchMenu = true
)
if (savedInstanceState == null) {
checkAndDeleteOldRecycleBinMessages()
handleAppPasswordProtection {
wasProtectionHandled = it
if (it) {
clearAllMessagesIfNeeded {
loadMessages()
}
} else {
finish()
}
}
}
if (checkAppSideloading()) {
return
}
}
override fun onResume() {
super.onResume()
updateMenuColors()
refreshMenuItems()
getOrCreateConversationsAdapter().apply {
if (storedTextColor != getProperTextColor()) {
updateTextColor(getProperTextColor())
}
if (storedFontSize != config.fontSize) {
updateFontSize()
}
updateDrafts()
}
updateTextColors(binding.mainCoordinator)
binding.searchHolder.setBackgroundColor(getProperBackgroundColor())
val properPrimaryColor = getProperPrimaryColor()
binding.noConversationsPlaceholder2.setTextColor(properPrimaryColor)
binding.noConversationsPlaceholder2.underlineText()
binding.conversationsFastscroller.updateColors(properPrimaryColor)
binding.conversationsProgressBar.setIndicatorColor(properPrimaryColor)
binding.conversationsProgressBar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA)
checkShortcut()
(binding.conversationsFab.layoutParams as? CoordinatorLayout.LayoutParams)?.bottomMargin =
navigationBarHeight + resources.getDimension(org.fossify.commons.R.dimen.activity_margin).toInt()
}
override fun onPause() {
super.onPause()
storeStateVariables()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
override fun onBackPressed() {
if (binding.mainMenu.isSearchOpen) {
binding.mainMenu.closeSearch()
} else {
super.onBackPressed()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(WAS_PROTECTION_HANDLED, wasProtectionHandled)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
wasProtectionHandled = savedInstanceState.getBoolean(WAS_PROTECTION_HANDLED, false)
if (!wasProtectionHandled) {
handleAppPasswordProtection {
wasProtectionHandled = it
if (it) {
loadMessages()
} else {
finish()
}
}
} else {
loadMessages()
}
}
private fun setupOptionsMenu() {
binding.mainMenu.getToolbar().inflateMenu(R.menu.menu_main)
binding.mainMenu.toggleHideOnScroll(true)
binding.mainMenu.setupMenu()
binding.mainMenu.onSearchClosedListener = {
fadeOutSearch()
}
binding.mainMenu.onSearchTextChangedListener = { text ->
if (text.isNotEmpty()) {
if (binding.searchHolder.alpha < 1f) {
binding.searchHolder.fadeIn()
}
} else {
fadeOutSearch()
}
searchTextChanged(text)
}
binding.mainMenu.getToolbar().setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.show_recycle_bin -> launchRecycleBin()
R.id.show_archived -> launchArchivedConversations()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun refreshMenuItems() {
binding.mainMenu.getToolbar().menu.apply {
findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)
findItem(R.id.show_recycle_bin).isVisible = config.useRecycleBin
findItem(R.id.show_archived).isVisible = config.isArchiveAvailable
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == MAKE_DEFAULT_APP_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
askPermissions()
} else {
finish()
}
}
}
private fun storeStateVariables() {
storedTextColor = getProperTextColor()
storedFontSize = config.fontSize
}
private fun updateMenuColors() {
updateStatusbarColor(getProperBackgroundColor())
binding.mainMenu.updateColors()
}
private fun loadMessages() {
if (isQPlus()) {
val roleManager = getSystemService(RoleManager::class.java)
if (roleManager!!.isRoleAvailable(RoleManager.ROLE_SMS)) {
if (roleManager.isRoleHeld(RoleManager.ROLE_SMS)) {
askPermissions()
} else {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS)
startActivityForResult(intent, MAKE_DEFAULT_APP_REQUEST)
}
} else {
toast(org.fossify.commons.R.string.unknown_error_occurred)
finish()
}
} else {
if (Telephony.Sms.getDefaultSmsPackage(this) == packageName) {
askPermissions()
} else {
val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, packageName)
startActivityForResult(intent, MAKE_DEFAULT_APP_REQUEST)
}
}
}
// 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() {
handlePermission(PERMISSION_READ_SMS) {
if (it) {
handlePermission(PERMISSION_SEND_SMS) {
if (it) {
handlePermission(PERMISSION_READ_CONTACTS) {
handleNotificationPermission { granted ->
if (!granted) {
PermissionRequiredDialog(
activity = this,
textId = org.fossify.commons.R.string.allow_notifications_incoming_messages,
positiveActionCallback = { openNotificationSettings() })
}
}
initMessenger()
bus = EventBus.getDefault()
try {
bus!!.register(this)
} catch (ignored: Exception) {
}
}
} else {
finish()
}
}
} else {
finish()
}
}
}
private fun initMessenger() {
checkWhatsNewDialog()
storeStateVariables()
getCachedConversations()
binding.noConversationsPlaceholder2.setOnClickListener {
launchNewConversation()
}
binding.conversationsFab.setOnClickListener {
launchNewConversation()
}
}
private fun getCachedConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getNonArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
val archived = try {
conversationsDB.getAllArchived()
} catch (e: Exception) {
listOf()
}
updateUnreadCountBadge(conversations)
runOnUiThread {
setupConversations(conversations, cached = true)
getNewConversations((conversations + archived).toMutableList() as ArrayList<Conversation>)
}
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
}
}
}
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread {
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val conversations = getConversations(privateContacts = privateContacts)
conversations.forEach { clonedConversation ->
val threadIds = cachedConversations.map { it.threadId }
if (!threadIds.contains(clonedConversation.threadId)) {
conversationsDB.insertOrUpdate(clonedConversation)
cachedConversations.add(clonedConversation)
}
}
cachedConversations.forEach { cachedConversation ->
val threadId = cachedConversation.threadId
val isTemporaryThread = cachedConversation.isScheduled
val isConversationDeleted = !conversations.map { it.threadId }.contains(threadId)
if (isConversationDeleted && !isTemporaryThread) {
conversationsDB.deleteThreadId(threadId)
}
val newConversation = conversations.find { it.phoneNumber == cachedConversation.phoneNumber }
if (isTemporaryThread && newConversation != null) {
// delete the original temporary thread and move any scheduled messages to the new thread
conversationsDB.deleteThreadId(threadId)
messagesDB.getScheduledThreadMessages(threadId)
.forEach { message ->
messagesDB.insertOrUpdate(message.copy(threadId = newConversation.threadId))
}
insertOrUpdateConversation(newConversation, cachedConversation)
}
}
cachedConversations.forEach { cachedConv ->
val conv = conversations.find {
it.threadId == cachedConv.threadId && !Conversation.areContentsTheSame(cachedConv, it)
}
if (conv != null) {
val lastModified = maxOf(cachedConv.date, conv.date)
val conversation = conv.copy(date = lastModified)
insertOrUpdateConversation(conversation)
}
}
val allConversations = conversationsDB.getNonArchived() as ArrayList<Conversation>
runOnUiThread {
setupConversations(allConversations)
}
if (config.appRunCount == 1) {
conversations.map { it.threadId }.forEach { threadId ->
val messages = getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false)
messages.chunked(30).forEach { currentMessages ->
messagesDB.insertMessages(*currentMessages.toTypedArray())
}
}
}
}
}
private fun getOrCreateConversationsAdapter(): ConversationsAdapter {
var currAdapter = binding.conversationsList.adapter
if (currAdapter == null) {
hideKeyboard()
currAdapter = ConversationsAdapter(
activity = this,
recyclerView = binding.conversationsList,
onRefresh = { notifyDatasetChanged() },
itemClick = { handleConversationClick(it) }
)
binding.conversationsList.adapter = currAdapter
if (areSystemAnimationsEnabled) {
binding.conversationsList.scheduleLayoutAnimation()
}
}
return currAdapter as ConversationsAdapter
}
private fun setupConversations(conversations: ArrayList<Conversation>, cached: Boolean = false) {
val sortedConversations = conversations.sortedWith(
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) }
.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
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
showOrHideProgress(conversations.isEmpty())
} else {
showOrHideProgress(false)
showOrHidePlaceholder(conversations.isEmpty())
}
try {
getOrCreateConversationsAdapter().apply {
updateConversations(sortedConversations) {
if (!cached) {
showOrHidePlaceholder(currentList.isEmpty())
}
}
}
} catch (ignored: Exception) {
}
}
private fun showOrHideProgress(show: Boolean) {
if (show) {
binding.conversationsProgressBar.show()
binding.noConversationsPlaceholder.beVisible()
binding.noConversationsPlaceholder.text = getString(R.string.loading_messages)
} else {
binding.conversationsProgressBar.hide()
binding.noConversationsPlaceholder.beGone()
}
}
private fun showOrHidePlaceholder(show: Boolean) {
binding.conversationsFastscroller.beGoneIf(show)
binding.noConversationsPlaceholder.beVisibleIf(show)
binding.noConversationsPlaceholder.text = getString(R.string.no_conversations_found)
binding.noConversationsPlaceholder2.beVisibleIf(show)
}
private fun fadeOutSearch() {
binding.searchHolder.animate().alpha(0f).setDuration(SHORT_ANIMATION_DURATION).withEndAction {
binding.searchHolder.beGone()
searchTextChanged("", true)
}.start()
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyDatasetChanged() {
getOrCreateConversationsAdapter().notifyDataSetChanged()
}
private fun handleConversationClick(any: Any) {
Intent(this, ThreadActivity::class.java).apply {
val conversation = any as Conversation
putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
putExtra(WAS_PROTECTION_HANDLED, wasProtectionHandled)
startActivity(this)
}
}
private fun launchNewConversation() {
hideKeyboard()
Intent(this, NewConversationActivity::class.java).apply {
startActivity(this)
}
}
@SuppressLint("NewApi")
private fun checkShortcut() {
val appIconColor = config.appIconColor
if (isNougatMR1Plus() && config.lastHandledShortcutColor != appIconColor) {
val newConversation = getCreateNewContactShortcut(appIconColor)
val manager = getSystemService(ShortcutManager::class.java)
try {
manager.dynamicShortcuts = listOf(newConversation)
config.lastHandledShortcutColor = appIconColor
} catch (ignored: Exception) {
}
}
}
@SuppressLint("NewApi")
private fun getCreateNewContactShortcut(appIconColor: Int): ShortcutInfo {
val newEvent = getString(R.string.new_conversation)
val drawable = resources.getDrawable(org.fossify.commons.R.drawable.shortcut_plus)
(drawable as LayerDrawable).findDrawableByLayerId(org.fossify.commons.R.id.shortcut_plus_background).applyColorFilter(appIconColor)
val bmp = drawable.convertToBitmap()
val intent = Intent(this, NewConversationActivity::class.java)
intent.action = Intent.ACTION_VIEW
return ShortcutInfo.Builder(this, "new_conversation")
.setShortLabel(newEvent)
.setLongLabel(newEvent)
.setIcon(Icon.createWithBitmap(bmp))
.setIntent(intent)
.build()
}
private fun searchTextChanged(text: String, forceUpdate: Boolean = false) {
if (!binding.mainMenu.isSearchOpen && !forceUpdate) {
return
}
lastSearchedText = text
binding.searchPlaceholder2.beGoneIf(text.length >= 2)
if (text.length >= 2) {
ensureBackgroundThread {
val searchQuery = "%$text%"
val messages = messagesDB.getMessagesWithText(searchQuery)
val conversations = conversationsDB.getConversationsWithText(searchQuery)
if (text == lastSearchedText) {
showSearchResults(messages, conversations, text)
}
}
} else {
binding.searchPlaceholder.beVisible()
binding.searchResultsList.beGone()
}
}
private fun showSearchResults(messages: List<Message>, conversations: List<Conversation>, searchedText: String) {
val searchResults = ArrayList<SearchResult>()
conversations.forEach { conversation ->
val date = conversation.date.formatDateOrTime(this, true, true)
val searchResult = SearchResult(-1, conversation.title, conversation.phoneNumber, date, conversation.threadId, conversation.photoUri)
searchResults.add(searchResult)
}
messages.sortedByDescending { it.id }.forEach { message ->
var recipient = message.senderName
if (recipient.isEmpty() && message.participants.isNotEmpty()) {
val participantNames = message.participants.map { it.name }
recipient = TextUtils.join(", ", participantNames)
}
val date = message.date.formatDateOrTime(this, true, true)
val searchResult = SearchResult(message.id, recipient, message.body, date, message.threadId, message.senderPhotoUri)
searchResults.add(searchResult)
}
runOnUiThread {
binding.searchResultsList.beVisibleIf(searchResults.isNotEmpty())
binding.searchPlaceholder.beVisibleIf(searchResults.isEmpty())
val currAdapter = binding.searchResultsList.adapter
if (currAdapter == null) {
SearchResultsAdapter(this, searchResults, binding.searchResultsList, searchedText) {
hideKeyboard()
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, (it as SearchResult).threadId)
putExtra(THREAD_TITLE, it.title)
putExtra(SEARCHED_MESSAGE_ID, it.messageId)
startActivity(this)
}
}.apply {
binding.searchResultsList.adapter = this
}
} else {
(currAdapter as SearchResultsAdapter).updateItems(searchResults, searchedText)
}
}
}
private fun launchRecycleBin() {
hideKeyboard()
startActivity(Intent(applicationContext, RecycleBinConversationsActivity::class.java))
}
private fun launchArchivedConversations() {
hideKeyboard()
startActivity(Intent(applicationContext, ArchivedConversationsActivity::class.java))
}
private fun launchSettings() {
hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java))
}
private fun launchAbout() {
val licenses = LICENSE_EVENT_BUS or LICENSE_SMS_MMS or LICENSE_INDICATOR_FAST_SCROLL
val faqItems = arrayListOf(
FAQItem(R.string.faq_2_title, R.string.faq_2_text),
FAQItem(R.string.faq_3_title, R.string.faq_3_text),
FAQItem(org.fossify.commons.R.string.faq_9_title_commons, org.fossify.commons.R.string.faq_9_text_commons)
)
if (!resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations)) {
faqItems.add(FAQItem(org.fossify.commons.R.string.faq_2_title_commons, org.fossify.commons.R.string.faq_2_text_commons))
faqItems.add(FAQItem(org.fossify.commons.R.string.faq_6_title_commons, org.fossify.commons.R.string.faq_6_text_commons))
}
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
initMessenger()
}
private fun checkWhatsNewDialog() {
arrayListOf<Release>().apply {
checkWhatsNew(this, BuildConfig.VERSION_CODE)
}
}
}

View file

@ -0,0 +1,93 @@
package org.fossify.messages.activities
import android.os.Bundle
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.APP_ICON_IDS
import org.fossify.commons.helpers.APP_LAUNCHER_NAME
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.interfaces.RefreshRecyclerViewListener
import org.fossify.messages.R
import org.fossify.messages.databinding.ActivityManageBlockedKeywordsBinding
import org.fossify.messages.dialogs.AddBlockedKeywordDialog
import org.fossify.messages.dialogs.ManageBlockedKeywordsAdapter
import org.fossify.messages.extensions.config
import org.fossify.messages.extensions.toArrayList
class ManageBlockedKeywordsActivity : BaseSimpleActivity(), RefreshRecyclerViewListener {
override fun getAppIconIDs() = intent.getIntegerArrayListExtra(APP_ICON_IDS) ?: ArrayList()
override fun getAppLauncherName() = intent.getStringExtra(APP_LAUNCHER_NAME) ?: ""
private val binding by viewBinding(ActivityManageBlockedKeywordsBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
updateBlockedKeywords()
setupOptionsMenu()
updateMaterialActivityViews(
mainCoordinatorLayout = binding.blockKeywordsCoordinator,
nestedView = binding.manageBlockedKeywordsList,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.manageBlockedKeywordsList, toolbar = binding.blockKeywordsToolbar)
updateTextColors(binding.manageBlockedKeywordsWrapper)
binding.manageBlockedKeywordsPlaceholder2.apply {
underlineText()
setTextColor(getProperPrimaryColor())
setOnClickListener {
addOrEditBlockedKeyword()
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(binding.blockKeywordsToolbar, NavigationIcon.Arrow)
}
private fun setupOptionsMenu() {
binding.blockKeywordsToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.add_blocked_keyword -> {
addOrEditBlockedKeyword()
true
}
else -> false
}
}
}
override fun refreshItems() {
updateBlockedKeywords()
}
private fun updateBlockedKeywords() {
ensureBackgroundThread {
val blockedKeywords = config.blockedKeywords
runOnUiThread {
ManageBlockedKeywordsAdapter(this, blockedKeywords.toArrayList(), this, binding.manageBlockedKeywordsList) {
addOrEditBlockedKeyword(it as String)
}.apply {
binding.manageBlockedKeywordsList.adapter = this
}
binding.manageBlockedKeywordsPlaceholder.beVisibleIf(blockedKeywords.isEmpty())
binding.manageBlockedKeywordsPlaceholder2.beVisibleIf(blockedKeywords.isEmpty())
}
}
}
private fun addOrEditBlockedKeyword(keyword: String? = null) {
AddBlockedKeywordDialog(this, keyword) {
updateBlockedKeywords()
}
}
}

View file

@ -0,0 +1,263 @@
package org.fossify.messages.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.WindowManager
import android.widget.Toast
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import org.fossify.commons.dialogs.RadioGroupDialog
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.*
import org.fossify.commons.models.RadioItem
import org.fossify.commons.models.SimpleContact
import org.fossify.messages.R
import org.fossify.messages.adapters.ContactsAdapter
import org.fossify.messages.databinding.ActivityNewConversationBinding
import org.fossify.messages.databinding.ItemSuggestedContactBinding
import org.fossify.messages.extensions.getSuggestedContacts
import org.fossify.messages.extensions.getThreadId
import org.fossify.messages.helpers.*
import org.fossify.messages.messaging.isShortCodeWithLetters
import java.net.URLDecoder
import java.util.Locale
class NewConversationActivity : SimpleActivity() {
private var allContacts = ArrayList<SimpleContact>()
private var privateContacts = ArrayList<SimpleContact>()
private val binding by viewBinding(ActivityNewConversationBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.new_conversation)
updateTextColors(binding.newConversationHolder)
updateMaterialActivityViews(
mainCoordinatorLayout = binding.newConversationCoordinator,
nestedView = binding.contactsList,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.contactsList, toolbar = binding.newConversationToolbar)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
binding.newConversationAddress.requestFocus()
// READ_CONTACTS permission is not mandatory, but without it we won't be able to show any suggestions during typing
handlePermission(PERMISSION_READ_CONTACTS) {
initContacts()
}
}
override fun onResume() {
super.onResume()
setupToolbar(binding.newConversationToolbar, NavigationIcon.Arrow)
binding.noContactsPlaceholder2.setTextColor(getProperPrimaryColor())
binding.noContactsPlaceholder2.underlineText()
binding.suggestionsLabel.setTextColor(getProperPrimaryColor())
}
private fun initContacts() {
if (isThirdPartyIntent()) {
return
}
fetchContacts()
binding.newConversationAddress.onTextChangeListener { searchString ->
val filteredContacts = ArrayList<SimpleContact>()
allContacts.forEach { contact ->
if (contact.phoneNumbers.any { it.normalizedNumber.contains(searchString, true) } ||
contact.name.contains(searchString, true) ||
contact.name.contains(searchString.normalizeString(), true) ||
contact.name.normalizeString().contains(searchString, true)) {
filteredContacts.add(contact)
}
}
filteredContacts.sortWith(compareBy { !it.name.startsWith(searchString, true) })
setupAdapter(filteredContacts)
binding.newConversationConfirm.beVisibleIf(searchString.length > 2)
}
binding.newConversationConfirm.applyColorFilter(getProperTextColor())
binding.newConversationConfirm.setOnClickListener {
val number = binding.newConversationAddress.value
if (isShortCodeWithLetters(number)) {
binding.newConversationAddress.setText("")
toast(R.string.invalid_short_code, length = Toast.LENGTH_LONG)
return@setOnClickListener
}
launchThreadActivity(number, number)
}
binding.noContactsPlaceholder2.setOnClickListener {
handlePermission(PERMISSION_READ_CONTACTS) {
if (it) {
fetchContacts()
}
}
}
val properPrimaryColor = getProperPrimaryColor()
binding.contactsLetterFastscroller.textColor = getProperTextColor().getColorStateList()
binding.contactsLetterFastscroller.pressedTextColor = properPrimaryColor
binding.contactsLetterFastscrollerThumb.setupWithFastScroller(binding.contactsLetterFastscroller)
binding.contactsLetterFastscrollerThumb.textColor = properPrimaryColor.getContrastColor()
binding.contactsLetterFastscrollerThumb.thumbColor = properPrimaryColor.getColorStateList()
}
private fun isThirdPartyIntent(): Boolean {
if ((intent.action == Intent.ACTION_SENDTO || intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_VIEW) && intent.dataString != null) {
val number = intent.dataString!!.removePrefix("sms:").removePrefix("smsto:").removePrefix("mms").removePrefix("mmsto:").replace("+", "%2b").trim()
launchThreadActivity(URLDecoder.decode(number), "")
finish()
return true
}
return false
}
private fun fetchContacts() {
fillSuggestedContacts {
SimpleContactsHelper(this).getAvailableContacts(false) {
allContacts = it
if (privateContacts.isNotEmpty()) {
allContacts.addAll(privateContacts)
allContacts.sort()
}
runOnUiThread {
setupAdapter(allContacts)
}
}
}
}
private fun setupAdapter(contacts: ArrayList<SimpleContact>) {
val hasContacts = contacts.isNotEmpty()
binding.contactsList.beVisibleIf(hasContacts)
binding.noContactsPlaceholder.beVisibleIf(!hasContacts)
binding.noContactsPlaceholder2.beVisibleIf(!hasContacts && !hasPermission(PERMISSION_READ_CONTACTS))
if (!hasContacts) {
val placeholderText = if (hasPermission(PERMISSION_READ_CONTACTS)) {
org.fossify.commons.R.string.no_contacts_found
} else {
org.fossify.commons.R.string.no_access_to_contacts
}
binding.noContactsPlaceholder.text = getString(placeholderText)
}
val currAdapter = binding.contactsList.adapter
if (currAdapter == null) {
ContactsAdapter(this, contacts, binding.contactsList) {
hideKeyboard()
val contact = it as SimpleContact
val phoneNumbers = contact.phoneNumbers
if (phoneNumbers.size > 1) {
val primaryNumber = contact.phoneNumbers.find { it.isPrimary }
if (primaryNumber != null) {
launchThreadActivity(primaryNumber.value, contact.name)
} else {
val items = ArrayList<RadioItem>()
phoneNumbers.forEachIndexed { index, phoneNumber ->
val type = getPhoneNumberTypeText(phoneNumber.type, phoneNumber.label)
items.add(RadioItem(index, "${phoneNumber.normalizedNumber} ($type)", phoneNumber.normalizedNumber))
}
RadioGroupDialog(this, items) {
launchThreadActivity(it as String, contact.name)
}
}
} else {
launchThreadActivity(phoneNumbers.first().normalizedNumber, contact.name)
}
}.apply {
binding.contactsList.adapter = this
}
if (areSystemAnimationsEnabled) {
binding.contactsList.scheduleLayoutAnimation()
}
} else {
(currAdapter as ContactsAdapter).updateContacts(contacts)
}
setupLetterFastscroller(contacts)
}
private fun fillSuggestedContacts(callback: () -> Unit) {
val privateCursor = getMyContactsCursor(false, true)
ensureBackgroundThread {
privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val suggestions = getSuggestedContacts(privateContacts)
runOnUiThread {
binding.suggestionsHolder.removeAllViews()
if (suggestions.isEmpty()) {
binding.suggestionsLabel.beGone()
binding.suggestionsScrollview.beGone()
} else {
binding.suggestionsLabel.beVisible()
binding.suggestionsScrollview.beVisible()
suggestions.forEach {
val contact = it
ItemSuggestedContactBinding.inflate(layoutInflater).apply {
suggestedContactName.text = contact.name
suggestedContactName.setTextColor(getProperTextColor())
if (!isDestroyed) {
SimpleContactsHelper(this@NewConversationActivity).loadContactImage(contact.photoUri, suggestedContactImage, contact.name)
binding.suggestionsHolder.addView(root)
root.setOnClickListener {
launchThreadActivity(contact.phoneNumbers.first().normalizedNumber, contact.name)
}
}
}
}
}
callback()
}
}
}
private fun setupLetterFastscroller(contacts: ArrayList<SimpleContact>) {
binding.contactsLetterFastscroller.setupWithRecyclerView(binding.contactsList, { position ->
try {
val name = contacts[position].name
val character = if (name.isNotEmpty()) name.substring(0, 1) else ""
FastScrollItemIndicator.Text(character.uppercase(Locale.getDefault()).normalizeString())
} catch (e: Exception) {
FastScrollItemIndicator.Text("")
}
})
}
private fun launchThreadActivity(phoneNumber: String, name: String) {
hideKeyboard()
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.getStringExtra("sms_body") ?: ""
val numbers = phoneNumber.split(";").toSet()
val number = if (numbers.size == 1) phoneNumber else Gson().toJson(numbers)
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, getThreadId(numbers))
putExtra(THREAD_TITLE, name)
putExtra(THREAD_TEXT, text)
putExtra(THREAD_NUMBER, number)
if (intent.action == Intent.ACTION_SEND && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
putExtra(THREAD_ATTACHMENT_URI, uri?.toString())
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
putExtra(THREAD_ATTACHMENT_URIS, uris)
}
startActivity(this)
}
}
}

View file

@ -0,0 +1,179 @@
package org.fossify.messages.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import org.fossify.commons.dialogs.ConfirmationDialog
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.WAS_PROTECTION_HANDLED
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.messages.R
import org.fossify.messages.adapters.RecycleBinConversationsAdapter
import org.fossify.messages.databinding.ActivityRecycleBinConversationsBinding
import org.fossify.messages.extensions.config
import org.fossify.messages.extensions.conversationsDB
import org.fossify.messages.extensions.emptyMessagesRecycleBin
import org.fossify.messages.helpers.IS_RECYCLE_BIN
import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.helpers.THREAD_TITLE
import org.fossify.messages.models.Conversation
import org.fossify.messages.models.Events
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class RecycleBinConversationsActivity : SimpleActivity() {
private var bus: EventBus? = null
private val binding by viewBinding(ActivityRecycleBinConversationsBinding::inflate)
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupOptionsMenu()
updateMaterialActivityViews(
mainCoordinatorLayout = binding.recycleBinCoordinator,
nestedView = binding.conversationsList,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.conversationsList, toolbar = binding.recycleBinToolbar)
loadRecycleBinConversations()
}
override fun onResume() {
super.onResume()
setupToolbar(binding.recycleBinToolbar, NavigationIcon.Arrow)
updateMenuColors()
loadRecycleBinConversations()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
private fun setupOptionsMenu() {
binding.recycleBinToolbar.inflateMenu(R.menu.recycle_bin_menu)
binding.recycleBinToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.empty_recycle_bin -> removeAll()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun updateOptionsMenu(conversations: ArrayList<Conversation>) {
binding.recycleBinToolbar.menu.apply {
findItem(R.id.empty_recycle_bin).isVisible = conversations.isNotEmpty()
}
}
private fun updateMenuColors() {
updateStatusbarColor(getProperBackgroundColor())
}
private fun loadRecycleBinConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getAllWithMessagesInRecycleBin().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
runOnUiThread {
setupConversations(conversations)
}
}
bus = EventBus.getDefault()
try {
bus!!.register(this)
} catch (ignored: Exception) {
}
}
private fun removeAll() {
ConfirmationDialog(
activity = this,
message = "",
messageId = R.string.empty_recycle_bin_messages_confirmation,
positive = org.fossify.commons.R.string.yes,
negative = org.fossify.commons.R.string.no
) {
ensureBackgroundThread {
emptyMessagesRecycleBin()
loadRecycleBinConversations()
}
}
}
private fun getOrCreateConversationsAdapter(): RecycleBinConversationsAdapter {
var currAdapter = binding.conversationsList.adapter
if (currAdapter == null) {
hideKeyboard()
currAdapter = RecycleBinConversationsAdapter(
activity = this,
recyclerView = binding.conversationsList,
onRefresh = { notifyDatasetChanged() },
itemClick = { handleConversationClick(it) }
)
binding.conversationsList.adapter = currAdapter
if (areSystemAnimationsEnabled) {
binding.conversationsList.scheduleLayoutAnimation()
}
}
return currAdapter as RecycleBinConversationsAdapter
}
private fun setupConversations(conversations: ArrayList<Conversation>) {
val sortedConversations = conversations.sortedWith(
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) }
.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
showOrHidePlaceholder(conversations.isEmpty())
updateOptionsMenu(conversations)
try {
getOrCreateConversationsAdapter().apply {
updateConversations(sortedConversations)
}
} catch (ignored: Exception) {
}
}
private fun showOrHidePlaceholder(show: Boolean) {
binding.conversationsFastscroller.beGoneIf(show)
binding.noConversationsPlaceholder.beVisibleIf(show)
binding.noConversationsPlaceholder.text = getString(R.string.no_conversations_found)
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyDatasetChanged() {
getOrCreateConversationsAdapter().notifyDataSetChanged()
}
private fun handleConversationClick(any: Any) {
Intent(this, ThreadActivity::class.java).apply {
val conversation = any as Conversation
putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
putExtra(WAS_PROTECTION_HANDLED, true)
putExtra(IS_RECYCLE_BIN, true)
startActivity(this)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
loadRecycleBinConversations()
}
}

View file

@ -0,0 +1,418 @@
package org.fossify.messages.activities
import android.annotation.TargetApi
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.fossify.commons.activities.ManageBlockedNumbersActivity
import org.fossify.commons.dialogs.*
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.*
import org.fossify.commons.models.RadioItem
import org.fossify.messages.R
import org.fossify.messages.databinding.ActivitySettingsBinding
import org.fossify.messages.dialogs.ExportMessagesDialog
import org.fossify.messages.extensions.config
import org.fossify.messages.extensions.emptyMessagesRecycleBin
import org.fossify.messages.extensions.messagesDB
import org.fossify.messages.helpers.*
import java.util.Locale
import kotlin.system.exitProcess
class SettingsActivity : SimpleActivity() {
private var blockedNumbersAtPause = -1
private var recycleBinMessages = 0
private val messagesFileType = "application/json"
private val messageImportFileTypes = listOf("application/json", "application/xml", "text/xml")
private val binding by viewBinding(ActivitySettingsBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
updateMaterialActivityViews(
mainCoordinatorLayout = binding.settingsCoordinator,
nestedView = binding.settingsHolder,
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.settingsNestedScrollview, toolbar = binding.settingsToolbar)
}
override fun onResume() {
super.onResume()
setupToolbar(binding.settingsToolbar, NavigationIcon.Arrow)
setupPurchaseThankYou()
setupCustomizeColors()
setupCustomizeNotifications()
setupUseEnglish()
setupLanguage()
setupManageBlockedNumbers()
setupManageBlockedKeywords()
setupChangeDateTimeFormat()
setupFontSize()
setupShowCharacterCounter()
setupUseSimpleCharacters()
setupSendOnEnter()
setupEnableDeliveryReports()
setupSendLongMessageAsMMS()
setupGroupMessageAsMMS()
setupLockScreenVisibility()
setupMMSFileSizeLimit()
setupUseRecycleBin()
setupEmptyRecycleBin()
setupAppPasswordProtection()
setupMessagesExport()
setupMessagesImport()
updateTextColors(binding.settingsNestedScrollview)
if (blockedNumbersAtPause != -1 && blockedNumbersAtPause != getBlockedNumbers().hashCode()) {
refreshMessages()
}
arrayOf(
binding.settingsColorCustomizationSectionLabel,
binding.settingsGeneralSettingsLabel,
binding.settingsOutgoingMessagesLabel,
binding.settingsNotificationsLabel,
binding.settingsRecycleBinLabel,
binding.settingsSecurityLabel,
binding.settingsMigratingLabel
).forEach {
it.setTextColor(getProperPrimaryColor())
}
}
private val getContent = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
MessagesImporter(this).importMessages(uri)
}
}
private val saveDocument = registerForActivityResult(ActivityResultContracts.CreateDocument(messagesFileType)) { uri ->
if (uri != null) {
toast(org.fossify.commons.R.string.exporting)
exportMessages(uri)
}
}
private fun setupMessagesExport() {
binding.settingsExportMessagesHolder.setOnClickListener {
ExportMessagesDialog(this) { fileName ->
saveDocument.launch(fileName)
}
}
}
private fun setupMessagesImport() {
binding.settingsImportMessagesHolder.setOnClickListener {
getContent.launch(messageImportFileTypes.toTypedArray())
}
}
private fun exportMessages(uri: Uri) {
ensureBackgroundThread {
try {
MessagesReader(this).getMessagesToExport(config.exportSms, config.exportMms) { messagesToExport ->
if (messagesToExport.isEmpty()) {
toast(org.fossify.commons.R.string.no_entries_for_exporting)
return@getMessagesToExport
}
val json = Json { encodeDefaults = true }
val jsonString = json.encodeToString(messagesToExport)
val outputStream = contentResolver.openOutputStream(uri)!!
outputStream.use {
it.write(jsonString.toByteArray())
}
toast(org.fossify.commons.R.string.exporting_successful)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
}
override fun onPause() {
super.onPause()
blockedNumbersAtPause = getBlockedNumbers().hashCode()
}
private fun setupPurchaseThankYou() = binding.apply {
settingsPurchaseThankYouHolder.beGoneIf(isOrWasThankYouInstalled())
settingsPurchaseThankYouHolder.setOnClickListener {
launchPurchaseThankYouIntent()
}
}
private fun setupCustomizeColors() = binding.apply {
settingsColorCustomizationLabel.text = getCustomizeColorsString()
settingsColorCustomizationHolder.setOnClickListener {
handleCustomizeColorsClick()
}
}
private fun setupCustomizeNotifications() = binding.apply {
settingsCustomizeNotificationsHolder.beVisibleIf(isOreoPlus())
settingsCustomizeNotificationsHolder.setOnClickListener {
launchCustomizeNotificationsIntent()
}
}
private fun setupUseEnglish() = binding.apply {
settingsUseEnglishHolder.beVisibleIf((config.wasUseEnglishToggled || Locale.getDefault().language != "en") && !isTiramisuPlus())
settingsUseEnglish.isChecked = config.useEnglish
settingsUseEnglishHolder.setOnClickListener {
settingsUseEnglish.toggle()
config.useEnglish = settingsUseEnglish.isChecked
exitProcess(0)
}
}
private fun setupLanguage() = binding.apply {
settingsLanguage.text = Locale.getDefault().displayLanguage
settingsLanguageHolder.beVisibleIf(isTiramisuPlus())
settingsLanguageHolder.setOnClickListener {
launchChangeAppLanguageIntent()
}
}
// support for device-wise blocking came on Android 7, rely only on that
@TargetApi(Build.VERSION_CODES.N)
private fun setupManageBlockedNumbers() = binding.apply {
settingsManageBlockedNumbers.text = addLockedLabelIfNeeded(org.fossify.commons.R.string.manage_blocked_numbers)
settingsManageBlockedNumbersHolder.beVisibleIf(isNougatPlus())
settingsManageBlockedNumbersHolder.setOnClickListener {
if (isOrWasThankYouInstalled()) {
Intent(this@SettingsActivity, ManageBlockedNumbersActivity::class.java).apply {
startActivity(this)
}
} else {
FeatureLockedDialog(this@SettingsActivity) { }
}
}
}
private fun setupManageBlockedKeywords() = binding.apply {
settingsManageBlockedKeywords.text = addLockedLabelIfNeeded(R.string.manage_blocked_keywords)
settingsManageBlockedKeywordsHolder.setOnClickListener {
if (isOrWasThankYouInstalled()) {
Intent(this@SettingsActivity, ManageBlockedKeywordsActivity::class.java).apply {
startActivity(this)
}
} else {
FeatureLockedDialog(this@SettingsActivity) { }
}
}
}
private fun setupChangeDateTimeFormat() = binding.apply {
settingsChangeDateTimeFormatHolder.setOnClickListener {
ChangeDateTimeFormatDialog(this@SettingsActivity) {
refreshMessages()
}
}
}
private fun setupFontSize() = binding.apply {
settingsFontSize.text = getFontSizeText()
settingsFontSizeHolder.setOnClickListener {
val items = arrayListOf(
RadioItem(FONT_SIZE_SMALL, getString(org.fossify.commons.R.string.small)),
RadioItem(FONT_SIZE_MEDIUM, getString(org.fossify.commons.R.string.medium)),
RadioItem(FONT_SIZE_LARGE, getString(org.fossify.commons.R.string.large)),
RadioItem(FONT_SIZE_EXTRA_LARGE, getString(org.fossify.commons.R.string.extra_large))
)
RadioGroupDialog(this@SettingsActivity, items, config.fontSize) {
config.fontSize = it as Int
settingsFontSize.text = getFontSizeText()
}
}
}
private fun setupShowCharacterCounter() = binding.apply {
settingsShowCharacterCounter.isChecked = config.showCharacterCounter
settingsShowCharacterCounterHolder.setOnClickListener {
settingsShowCharacterCounter.toggle()
config.showCharacterCounter = settingsShowCharacterCounter.isChecked
}
}
private fun setupUseSimpleCharacters() = binding.apply {
settingsUseSimpleCharacters.isChecked = config.useSimpleCharacters
settingsUseSimpleCharactersHolder.setOnClickListener {
settingsUseSimpleCharacters.toggle()
config.useSimpleCharacters = settingsUseSimpleCharacters.isChecked
}
}
private fun setupSendOnEnter() = binding.apply {
settingsSendOnEnter.isChecked = config.sendOnEnter
settingsSendOnEnterHolder.setOnClickListener {
settingsSendOnEnter.toggle()
config.sendOnEnter = settingsSendOnEnter.isChecked
}
}
private fun setupEnableDeliveryReports() = binding.apply {
settingsEnableDeliveryReports.isChecked = config.enableDeliveryReports
settingsEnableDeliveryReportsHolder.setOnClickListener {
settingsEnableDeliveryReports.toggle()
config.enableDeliveryReports = settingsEnableDeliveryReports.isChecked
}
}
private fun setupSendLongMessageAsMMS() = binding.apply {
settingsSendLongMessageMms.isChecked = config.sendLongMessageMMS
settingsSendLongMessageMmsHolder.setOnClickListener {
settingsSendLongMessageMms.toggle()
config.sendLongMessageMMS = settingsSendLongMessageMms.isChecked
}
}
private fun setupGroupMessageAsMMS() = binding.apply {
settingsSendGroupMessageMms.isChecked = config.sendGroupMessageMMS
settingsSendGroupMessageMmsHolder.setOnClickListener {
settingsSendGroupMessageMms.toggle()
config.sendGroupMessageMMS = settingsSendGroupMessageMms.isChecked
}
}
private fun setupLockScreenVisibility() = binding.apply {
settingsLockScreenVisibility.text = getLockScreenVisibilityText()
settingsLockScreenVisibilityHolder.setOnClickListener {
val items = arrayListOf(
RadioItem(LOCK_SCREEN_SENDER_MESSAGE, getString(R.string.sender_and_message)),
RadioItem(LOCK_SCREEN_SENDER, getString(R.string.sender_only)),
RadioItem(LOCK_SCREEN_NOTHING, getString(org.fossify.commons.R.string.nothing)),
)
RadioGroupDialog(this@SettingsActivity, items, config.lockScreenVisibilitySetting) {
config.lockScreenVisibilitySetting = it as Int
settingsLockScreenVisibility.text = getLockScreenVisibilityText()
}
}
}
private fun getLockScreenVisibilityText() = getString(
when (config.lockScreenVisibilitySetting) {
LOCK_SCREEN_SENDER_MESSAGE -> R.string.sender_and_message
LOCK_SCREEN_SENDER -> R.string.sender_only
else -> org.fossify.commons.R.string.nothing
}
)
private fun setupMMSFileSizeLimit() = binding.apply {
settingsMmsFileSizeLimit.text = getMMSFileLimitText()
settingsMmsFileSizeLimitHolder.setOnClickListener {
val items = arrayListOf(
RadioItem(7, getString(R.string.mms_file_size_limit_none), FILE_SIZE_NONE),
RadioItem(6, getString(R.string.mms_file_size_limit_2mb), FILE_SIZE_2_MB),
RadioItem(5, getString(R.string.mms_file_size_limit_1mb), FILE_SIZE_1_MB),
RadioItem(4, getString(R.string.mms_file_size_limit_600kb), FILE_SIZE_600_KB),
RadioItem(3, getString(R.string.mms_file_size_limit_300kb), FILE_SIZE_300_KB),
RadioItem(2, getString(R.string.mms_file_size_limit_200kb), FILE_SIZE_200_KB),
RadioItem(1, getString(R.string.mms_file_size_limit_100kb), FILE_SIZE_100_KB),
)
val checkedItemId = items.find { it.value == config.mmsFileSizeLimit }?.id ?: 7
RadioGroupDialog(this@SettingsActivity, items, checkedItemId) {
config.mmsFileSizeLimit = it as Long
settingsMmsFileSizeLimit.text = getMMSFileLimitText()
}
}
}
private fun setupUseRecycleBin() = binding.apply {
updateRecycleBinButtons()
settingsUseRecycleBin.isChecked = config.useRecycleBin
settingsUseRecycleBinHolder.setOnClickListener {
settingsUseRecycleBin.toggle()
config.useRecycleBin = settingsUseRecycleBin.isChecked
updateRecycleBinButtons()
}
}
private fun updateRecycleBinButtons() = binding.apply {
settingsEmptyRecycleBinHolder.beVisibleIf(config.useRecycleBin)
}
private fun setupEmptyRecycleBin() = binding.apply {
ensureBackgroundThread {
recycleBinMessages = messagesDB.getArchivedCount()
runOnUiThread {
settingsEmptyRecycleBinSize.text =
resources.getQuantityString(R.plurals.delete_messages, recycleBinMessages, recycleBinMessages)
}
}
settingsEmptyRecycleBinHolder.setOnClickListener {
if (recycleBinMessages == 0) {
toast(org.fossify.commons.R.string.recycle_bin_empty)
} else {
ConfirmationDialog(
activity = this@SettingsActivity,
message = "",
messageId = R.string.empty_recycle_bin_messages_confirmation,
positive = org.fossify.commons.R.string.yes,
negative = org.fossify.commons.R.string.no
) {
ensureBackgroundThread {
emptyMessagesRecycleBin()
}
recycleBinMessages = 0
settingsEmptyRecycleBinSize.text =
resources.getQuantityString(R.plurals.delete_messages, recycleBinMessages, recycleBinMessages)
}
}
}
}
private fun setupAppPasswordProtection() = binding.apply {
settingsAppPasswordProtection.isChecked = config.isAppPasswordProtectionOn
settingsAppPasswordProtectionHolder.setOnClickListener {
val tabToShow = if (config.isAppPasswordProtectionOn) config.appProtectionType else SHOW_ALL_TABS
SecurityDialog(this@SettingsActivity, config.appPasswordHash, tabToShow) { hash, type, success ->
if (success) {
val hasPasswordProtection = config.isAppPasswordProtectionOn
settingsAppPasswordProtection.isChecked = !hasPasswordProtection
config.isAppPasswordProtectionOn = !hasPasswordProtection
config.appPasswordHash = if (hasPasswordProtection) "" else hash
config.appProtectionType = type
if (config.isAppPasswordProtectionOn) {
val confirmationTextId = if (config.appProtectionType == PROTECTION_FINGERPRINT) {
org.fossify.commons.R.string.fingerprint_setup_successfully
} else {
org.fossify.commons.R.string.protection_setup_successfully
}
ConfirmationDialog(this@SettingsActivity, "", confirmationTextId, org.fossify.commons.R.string.ok, 0) { }
}
}
}
}
}
private fun getMMSFileLimitText() = getString(
when (config.mmsFileSizeLimit) {
FILE_SIZE_100_KB -> R.string.mms_file_size_limit_100kb
FILE_SIZE_200_KB -> R.string.mms_file_size_limit_200kb
FILE_SIZE_300_KB -> R.string.mms_file_size_limit_300kb
FILE_SIZE_600_KB -> R.string.mms_file_size_limit_600kb
FILE_SIZE_1_MB -> R.string.mms_file_size_limit_1mb
FILE_SIZE_2_MB -> R.string.mms_file_size_limit_2mb
else -> R.string.mms_file_size_limit_none
}
)
}

View file

@ -0,0 +1,30 @@
package org.fossify.messages.activities
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.messages.R
open class SimpleActivity : BaseSimpleActivity() {
override fun getAppIconIDs() = arrayListOf(
R.mipmap.ic_launcher_red,
R.mipmap.ic_launcher_pink,
R.mipmap.ic_launcher_purple,
R.mipmap.ic_launcher_deep_purple,
R.mipmap.ic_launcher_indigo,
R.mipmap.ic_launcher_blue,
R.mipmap.ic_launcher_light_blue,
R.mipmap.ic_launcher_cyan,
R.mipmap.ic_launcher_teal,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher_light_green,
R.mipmap.ic_launcher_lime,
R.mipmap.ic_launcher_yellow,
R.mipmap.ic_launcher_amber,
R.mipmap.ic_launcher_orange,
R.mipmap.ic_launcher_deep_orange,
R.mipmap.ic_launcher_brown,
R.mipmap.ic_launcher_blue_grey,
R.mipmap.ic_launcher_grey_black
)
override fun getAppLauncherName() = getString(R.string.app_launcher_name)
}

View file

@ -0,0 +1,11 @@
package org.fossify.messages.activities
import android.content.Intent
import org.fossify.commons.activities.BaseSplashActivity
class SplashActivity : BaseSplashActivity() {
override fun initActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
package org.fossify.messages.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import ezvcard.VCard
import ezvcard.property.Email
import ezvcard.property.Telephone
import org.fossify.commons.extensions.normalizePhoneNumber
import org.fossify.commons.extensions.sendEmailIntent
import org.fossify.commons.extensions.viewBinding
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.messages.R
import org.fossify.messages.adapters.VCardViewerAdapter
import org.fossify.messages.databinding.ActivityVcardViewerBinding
import org.fossify.messages.extensions.dialNumber
import org.fossify.messages.helpers.EXTRA_VCARD_URI
import org.fossify.messages.helpers.parseVCardFromUri
import org.fossify.messages.models.VCardPropertyWrapper
import org.fossify.messages.models.VCardWrapper
class VCardViewerActivity : SimpleActivity() {
private val binding by viewBinding(ActivityVcardViewerBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(binding.root)
updateMaterialActivityViews(binding.vcardViewerCoordinator, binding.contactsList, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(binding.contactsList, binding.vcardToolbar)
val vCardUri = intent.getParcelableExtra(EXTRA_VCARD_URI) as? Uri
if (vCardUri != null) {
setupOptionsMenu(vCardUri)
parseVCardFromUri(this, vCardUri) {
runOnUiThread {
setupContactsList(it)
}
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(binding.vcardToolbar, NavigationIcon.Arrow)
}
private fun setupOptionsMenu(vCardUri: Uri) {
binding.vcardToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.add_contact -> {
val intent = Intent(Intent.ACTION_VIEW).apply {
val mimetype = contentResolver.getType(vCardUri)
setDataAndType(vCardUri, mimetype?.lowercase())
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
}
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun setupContactsList(vCards: List<VCard>) {
val items = prepareData(vCards)
val adapter = VCardViewerAdapter(this, items.toMutableList()) { item ->
val property = item as? VCardPropertyWrapper
if (property != null) {
handleClick(item)
}
}
binding.contactsList.adapter = adapter
}
private fun handleClick(property: VCardPropertyWrapper) {
when (property.property) {
is Telephone -> dialNumber(property.value.normalizePhoneNumber())
is Email -> sendEmailIntent(property.value)
}
}
private fun prepareData(vCards: List<VCard>): List<VCardWrapper> {
return vCards.map { vCard -> VCardWrapper.from(this, vCard) }
}
}