Add support for archiving conversations

Archiving messages currently acts like recycle bin in
Simple Gallery, meaning that after 30 days, conversations
will be deleted permanently. Any updates to the conversation
(new messages) removes it from archive.

This closes #177
This commit is contained in:
Ensar Sarajčić 2023-07-11 16:52:47 +02:00
parent 9942fb788a
commit 47861f605d
22 changed files with 846 additions and 191 deletions

View File

@ -51,6 +51,13 @@
android:configChanges="orientation" android:configChanges="orientation"
android:exported="true" /> android:exported="true" />
<activity
android:name=".activities.ArchivedConversationsActivity"
android:configChanges="orientation"
android:exported="true"
android:label="Archived Conversations"
android:parentActivityName=".activities.MainActivity" />
<activity <activity
android:name=".activities.ThreadActivity" android:name=".activities.ThreadActivity"
android:configChanges="orientation" android:configChanges="orientation"

View File

@ -0,0 +1,150 @@
package com.simplemobiletools.smsmessenger.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ArchivedConversationsAdapter
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Events
import kotlinx.android.synthetic.main.activity_archived_conversations.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ArchivedConversationsActivity : SimpleActivity() {
private var bus: EventBus? = null
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_archived_conversations)
setupOptionsMenu()
updateMaterialActivityViews(archive_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(conversations_list, archive_toolbar)
loadArchivedConversations()
}
override fun onResume() {
super.onResume()
setupToolbar(archive_toolbar, NavigationIcon.Arrow)
updateMenuColors()
loadArchivedConversations()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
private fun setupOptionsMenu() {
archive_toolbar.inflateMenu(R.menu.archive_menu)
archive_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.empty_archive -> removeAll()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
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 (e: Exception) {
}
}
private fun removeAll() {
removeAllArchivedConversations {
loadArchivedConversations()
}
}
private fun getOrCreateConversationsAdapter(): ArchivedConversationsAdapter {
var currAdapter = conversations_list.adapter
if (currAdapter == null) {
hideKeyboard()
currAdapter = ArchivedConversationsAdapter(
activity = this,
recyclerView = conversations_list,
onRefresh = { notifyDatasetChanged() },
itemClick = { handleConversationClick(it) }
)
conversations_list.adapter = currAdapter
if (areSystemAnimationsEnabled) {
conversations_list.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())
try {
getOrCreateConversationsAdapter().apply {
updateConversations(sortedConversations)
}
} catch (ignored: Exception) {
}
}
private fun showOrHidePlaceholder(show: Boolean) {
conversations_fastscroller.beGoneIf(show)
no_conversations_placeholder.beVisibleIf(show)
no_conversations_placeholder.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)
startActivity(this)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
loadArchivedConversations()
}
}

View File

@ -64,6 +64,7 @@ class MainActivity : SimpleActivity() {
updateMaterialActivityViews(main_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = true) updateMaterialActivityViews(main_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = true)
if (savedInstanceState == null) { if (savedInstanceState == null) {
checkAndDeleteOldArchivedConversations()
handleAppPasswordProtection { handleAppPasswordProtection {
wasProtectionHandled = it wasProtectionHandled = it
if (it) { if (it) {
@ -177,6 +178,7 @@ class MainActivity : SimpleActivity() {
R.id.import_messages -> tryImportMessages() R.id.import_messages -> tryImportMessages()
R.id.export_messages -> tryToExportMessages() R.id.export_messages -> tryToExportMessages()
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.show_archived -> launchArchivedConversations()
R.id.settings -> launchSettings() R.id.settings -> launchSettings()
R.id.about -> launchAbout() R.id.about -> launchAbout()
else -> return@setOnMenuItemClickListener false else -> return@setOnMenuItemClickListener false
@ -289,15 +291,20 @@ class MainActivity : SimpleActivity() {
private fun getCachedConversations() { private fun getCachedConversations() {
ensureBackgroundThread { ensureBackgroundThread {
val conversations = try { val conversations = try {
conversationsDB.getAll().toMutableList() as ArrayList<Conversation> conversationsDB.getNonArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) { } catch (e: Exception) {
ArrayList() ArrayList()
} }
val archived = try {
conversationsDB.getAllArchived()
} catch (e: Exception) {
listOf()
}
updateUnreadCountBadge(conversations) updateUnreadCountBadge(conversations)
runOnUiThread { runOnUiThread {
setupConversations(conversations, cached = true) setupConversations(conversations, cached = true)
getNewConversations(conversations) getNewConversations((conversations + archived).toMutableList() as ArrayList<Conversation>)
} }
conversations.forEach { conversations.forEach {
clearExpiredScheduledMessages(it.threadId) clearExpiredScheduledMessages(it.threadId)
@ -351,7 +358,7 @@ class MainActivity : SimpleActivity() {
} }
} }
val allConversations = conversationsDB.getAll() as ArrayList<Conversation> val allConversations = conversationsDB.getNonArchived() as ArrayList<Conversation>
runOnUiThread { runOnUiThread {
setupConversations(allConversations) setupConversations(allConversations)
} }
@ -556,6 +563,11 @@ class MainActivity : SimpleActivity() {
} }
} }
private fun launchArchivedConversations() {
hideKeyboard()
startActivity(Intent(applicationContext, ArchivedConversationsActivity::class.java))
}
private fun launchSettings() { private fun launchSettings() {
hideKeyboard() hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java)) startActivity(Intent(applicationContext, SettingsActivity::class.java))

View File

@ -11,12 +11,15 @@ import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.conversationsDB
import com.simplemobiletools.smsmessenger.extensions.removeAllArchivedConversations
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.activity_settings.*
import java.util.* import java.util.*
class SettingsActivity : SimpleActivity() { class SettingsActivity : SimpleActivity() {
private var blockedNumbersAtPause = -1 private var blockedNumbersAtPause = -1
private var recycleBinConversations = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true isMaterialActivity = true
@ -47,6 +50,8 @@ class SettingsActivity : SimpleActivity() {
setupGroupMessageAsMMS() setupGroupMessageAsMMS()
setupLockScreenVisibility() setupLockScreenVisibility()
setupMMSFileSizeLimit() setupMMSFileSizeLimit()
setupUseRecycleBin()
setupEmptyRecycleBin()
setupAppPasswordProtection() setupAppPasswordProtection()
updateTextColors(settings_nested_scrollview) updateTextColors(settings_nested_scrollview)
@ -59,6 +64,7 @@ class SettingsActivity : SimpleActivity() {
settings_general_settings_label, settings_general_settings_label,
settings_outgoing_messages_label, settings_outgoing_messages_label,
settings_notifications_label, settings_notifications_label,
settings_recycle_bin_label,
settings_security_label settings_security_label
).forEach { ).forEach {
it.setTextColor(getProperPrimaryColor()) it.setTextColor(getProperPrimaryColor())
@ -244,6 +250,43 @@ class SettingsActivity : SimpleActivity() {
} }
} }
private fun setupUseRecycleBin() {
updateRecycleBinButtons()
settings_use_recycle_bin.isChecked = config.useArchive
settings_use_recycle_bin_holder.setOnClickListener {
settings_use_recycle_bin.toggle()
config.useArchive = settings_use_recycle_bin.isChecked
updateRecycleBinButtons()
}
}
private fun updateRecycleBinButtons() {
settings_empty_recycle_bin_holder.beVisibleIf(config.useArchive)
}
private fun setupEmptyRecycleBin() {
ensureBackgroundThread {
recycleBinConversations = conversationsDB.getArchivedCount()
runOnUiThread {
settings_empty_recycle_bin_size.text =
resources.getQuantityString(R.plurals.delete_conversations, recycleBinConversations, recycleBinConversations)
}
}
settings_empty_recycle_bin_holder.setOnClickListener {
if (recycleBinConversations == 0) {
toast(R.string.recycle_bin_empty)
} else {
ConfirmationDialog(this, "", R.string.empty_recycle_bin_confirmation, R.string.yes, R.string.no) {
removeAllArchivedConversations()
recycleBinConversations = 0
settings_empty_recycle_bin_size.text =
resources.getQuantityString(R.plurals.delete_conversations, recycleBinConversations, recycleBinConversations)
}
}
}
}
private fun setupAppPasswordProtection() { private fun setupAppPasswordProtection() {
settings_app_password_protection.isChecked = config.isAppPasswordProtectionOn settings_app_password_protection.isChecked = config.isAppPasswordProtectionOn
settings_app_password_protection_holder.setOnClickListener { settings_app_password_protection_holder.setOnClickListener {

View File

@ -55,6 +55,7 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AttachmentsAdapter import com.simplemobiletools.smsmessenger.adapters.AttachmentsAdapter
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.dialogs.DeleteConfirmationDialog
import com.simplemobiletools.smsmessenger.dialogs.InvalidNumberDialog import com.simplemobiletools.smsmessenger.dialogs.InvalidNumberDialog
import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
@ -888,9 +889,13 @@ class ThreadActivity : SimpleActivity() {
} }
private fun askConfirmDelete() { private fun askConfirmDelete() {
ConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation)) { DeleteConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation), config.useArchive) { skipRecycleBin ->
ensureBackgroundThread { ensureBackgroundThread {
deleteConversation(threadId) if (skipRecycleBin || config.useArchive.not()) {
deleteConversation(threadId)
} else {
moveConversationToRecycleBin(threadId)
}
runOnUiThread { runOnUiThread {
refreshMessages() refreshMessages()
finish() finish()
@ -1327,6 +1332,7 @@ class ThreadActivity : SimpleActivity() {
} }
} }
messagesDB.insertOrUpdate(message) messagesDB.insertOrUpdate(message)
conversationsDB.deleteThreadFromArchivedConversations(message.threadId)
} }
// show selected contacts, properly split to new lines when appropriate // show selected contacts, properly split to new lines when appropriate

View File

@ -0,0 +1,94 @@
package com.simplemobiletools.smsmessenger.adapters
import android.view.Menu
import com.simplemobiletools.commons.extensions.notificationManager
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.dialogs.DeleteConfirmationDialog
import com.simplemobiletools.smsmessenger.extensions.conversationsDB
import com.simplemobiletools.smsmessenger.extensions.deleteConversation
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.models.Conversation
class ArchivedConversationsAdapter(
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
override fun getActionMenuId() = R.menu.cab_archived_conversations
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_delete -> askConfirmDelete()
R.id.cab_unarchive -> ensureBackgroundThread { unarchiveConversation() }
R.id.cab_select_all -> selectAll()
}
}
private fun askConfirmDelete() {
val itemsCnt = selectedKeys.size
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
val baseString = R.string.deletion_confirmation
val question = String.format(resources.getString(baseString), items)
DeleteConfirmationDialog(activity, question, showSkipRecycleBinOption = false) { _ ->
ensureBackgroundThread {
deleteConversations()
}
}
}
private fun deleteConversations() {
if (selectedKeys.isEmpty()) {
return
}
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
conversationsToRemove.forEach {
activity.deleteConversation(it.threadId)
activity.notificationManager.cancel(it.threadId.hashCode())
}
removeConversationsFromList(conversationsToRemove)
}
private fun unarchiveConversation() {
if (selectedKeys.isEmpty()) {
return
}
val conversationsToUnarchive = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
conversationsToUnarchive.forEach {
activity.conversationsDB.deleteThreadFromArchivedConversations(it.threadId)
}
removeConversationsFromList(conversationsToUnarchive)
}
private fun removeConversationsFromList(removedConversations: List<Conversation>) {
val newList = try {
currentList.toMutableList().apply { removeAll(removedConversations) }
} catch (ignored: Exception) {
currentList.toMutableList()
}
activity.runOnUiThread {
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
refreshMessages()
finishActMode()
} else {
submitList(newList)
if (newList.isEmpty()) {
refreshMessages()
}
}
}
}
}

View File

@ -0,0 +1,183 @@
package com.simplemobiletools.smsmessenger.adapters
import android.graphics.Typeface
import android.os.Parcelable
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.*
@Suppress("LeakingThis")
abstract class BaseConversationsAdapter(
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
) : MyRecyclerViewListAdapter<Conversation>(activity, recyclerView, ConversationDiffCallback(), itemClick, onRefresh),
RecyclerViewFastScroller.OnPopupTextUpdate {
private var fontSize = activity.getTextSize()
private var drafts = HashMap<Long, String?>()
private var recyclerViewState: Parcelable? = null
init {
setupDragListener(true)
ensureBackgroundThread {
fetchDrafts(drafts)
}
setHasStableIds(true)
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() = restoreRecyclerViewState()
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = restoreRecyclerViewState()
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = restoreRecyclerViewState()
})
}
fun updateFontSize() {
fontSize = activity.getTextSize()
notifyDataSetChanged()
}
fun updateConversations(newConversations: ArrayList<Conversation>, commitCallback: (() -> Unit)? = null) {
saveRecyclerViewState()
submitList(newConversations.toList(), commitCallback)
}
fun updateDrafts() {
ensureBackgroundThread {
val newDrafts = HashMap<Long, String?>()
fetchDrafts(newDrafts)
if (drafts.hashCode() != newDrafts.hashCode()) {
drafts = newDrafts
activity.runOnUiThread {
notifyDataSetChanged()
}
}
}
}
override fun getSelectableItemCount() = itemCount
protected fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = currentList.getOrNull(position)?.hashCode()
override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val conversation = getItem(position)
holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ ->
setupView(itemView, conversation)
}
bindViewHolder(holder)
}
override fun getItemId(position: Int) = getItem(position).threadId
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed && !activity.isFinishing) {
Glide.with(activity).clear(holder.itemView.conversation_image)
}
}
private fun fetchDrafts(drafts: HashMap<Long, String?>) {
drafts.clear()
for ((threadId, draft) in activity.getAllDrafts()) {
drafts[threadId] = draft
}
}
private fun setupView(view: View, conversation: Conversation) {
view.apply {
setupViewBackground(activity)
val smsDraft = drafts[conversation.threadId]
draft_indicator.beVisibleIf(smsDraft != null)
draft_indicator.setTextColor(properPrimaryColor)
pin_indicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString()))
pin_indicator.applyColorFilter(textColor)
conversation_frame.isSelected = selectedKeys.contains(conversation.hashCode())
conversation_address.apply {
text = conversation.title
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
}
conversation_body_short.apply {
text = smsDraft ?: conversation.snippet
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f)
}
conversation_date.apply {
text = conversation.date.formatDateOrTime(context, true, false)
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
}
val style = if (conversation.read) {
conversation_body_short.alpha = 0.7f
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
} else {
conversation_body_short.alpha = 1f
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
}
conversation_address.setTypeface(null, style)
conversation_body_short.setTypeface(null, style)
arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
it.setTextColor(textColor)
}
// at group conversations we use an icon as the placeholder, not any letter
val placeholder = if (conversation.isGroupConversation) {
SimpleContactsHelper(context).getColoredGroupIcon(conversation.title)
} else {
null
}
SimpleContactsHelper(context).loadContactImage(conversation.photoUri, conversation_image, conversation.title, placeholder)
}
}
override fun onChange(position: Int) = currentList.getOrNull(position)?.title ?: ""
private fun saveRecyclerViewState() {
recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
}
private fun restoreRecyclerViewState() {
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
}
private class ConversationDiffCallback : DiffUtil.ItemCallback<Conversation>() {
override fun areItemsTheSame(oldItem: Conversation, newItem: Conversation): Boolean {
return Conversation.areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(oldItem: Conversation, newItem: Conversation): Boolean {
return Conversation.areContentsTheSame(oldItem, newItem)
}
}
}

View File

@ -1,59 +1,27 @@
package com.simplemobiletools.smsmessenger.adapters package com.simplemobiletools.smsmessenger.adapters
import android.content.Intent import android.content.Intent
import android.graphics.Typeface
import android.os.Parcelable
import android.text.TextUtils import android.text.TextUtils
import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter
import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FeatureLockedDialog import com.simplemobiletools.commons.dialogs.FeatureLockedDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.KEY_PHONE import com.simplemobiletools.commons.helpers.KEY_PHONE
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.dialogs.DeleteConfirmationDialog
import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters
import com.simplemobiletools.smsmessenger.models.Conversation import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.*
class ConversationsAdapter( class ConversationsAdapter(
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
) : MyRecyclerViewListAdapter<Conversation>(activity, recyclerView, ConversationDiffCallback(), itemClick, onRefresh), ) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
RecyclerViewFastScroller.OnPopupTextUpdate {
private var fontSize = activity.getTextSize()
private var drafts = HashMap<Long, String?>()
private var recyclerViewState: Parcelable? = null
init {
setupDragListener(true)
ensureBackgroundThread {
fetchDrafts(drafts)
}
setHasStableIds(true)
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() = restoreRecyclerViewState()
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = restoreRecyclerViewState()
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = restoreRecyclerViewState()
})
}
override fun getActionMenuId() = R.menu.cab_conversations override fun getActionMenuId() = R.menu.cab_conversations
override fun prepareActionMode(menu: Menu) { override fun prepareActionMode(menu: Menu) {
@ -95,37 +63,6 @@ class ConversationsAdapter(
} }
} }
override fun getSelectableItemCount() = itemCount
override fun getIsItemSelectable(position: Int) = true
override fun getItemSelectionKey(position: Int) = currentList.getOrNull(position)?.hashCode()
override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { it.hashCode() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val conversation = getItem(position)
holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ ->
setupView(itemView, conversation)
}
bindViewHolder(holder)
}
override fun getItemId(position: Int) = getItem(position).threadId
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed && !activity.isFinishing) {
Glide.with(activity).clear(holder.itemView.conversation_image)
}
}
private fun tryBlocking() { private fun tryBlocking() {
if (activity.isOrWasThankYouInstalled()) { if (activity.isOrWasThankYouInstalled()) {
askConfirmBlock() askConfirmBlock()
@ -184,21 +121,25 @@ class ConversationsAdapter(
val baseString = R.string.deletion_confirmation val baseString = R.string.deletion_confirmation
val question = String.format(resources.getString(baseString), items) val question = String.format(resources.getString(baseString), items)
ConfirmationDialog(activity, question) { DeleteConfirmationDialog(activity, question, activity.config.useArchive) { skipRecycleBin ->
ensureBackgroundThread { ensureBackgroundThread {
deleteConversations() deleteConversations(skipRecycleBin)
} }
} }
} }
private fun deleteConversations() { private fun deleteConversations(skipRecycleBin: Boolean) {
if (selectedKeys.isEmpty()) { if (selectedKeys.isEmpty()) {
return return
} }
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
conversationsToRemove.forEach { conversationsToRemove.forEach {
activity.deleteConversation(it.threadId) if (skipRecycleBin || activity.config.useArchive.not()) {
activity.deleteConversation(it.threadId)
} else {
activity.moveConversationToRecycleBin(it.threadId)
}
activity.notificationManager.cancel(it.threadId.hashCode()) activity.notificationManager.cancel(it.threadId.hashCode())
} }
@ -276,8 +217,6 @@ class ConversationsAdapter(
} }
} }
private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
private fun pinConversation(pin: Boolean) { private fun pinConversation(pin: Boolean) {
val conversations = getSelectedItems() val conversations = getSelectedItems()
if (conversations.isEmpty()) { if (conversations.isEmpty()) {
@ -303,113 +242,10 @@ class ConversationsAdapter(
menu.findItem(R.id.cab_unpin_conversation).isVisible = selectedConversations.any { pinnedConversations.contains(it.threadId.toString()) } menu.findItem(R.id.cab_unpin_conversation).isVisible = selectedConversations.any { pinnedConversations.contains(it.threadId.toString()) }
} }
private fun fetchDrafts(drafts: HashMap<Long, String?>) {
drafts.clear()
for ((threadId, draft) in activity.getAllDrafts()) {
drafts[threadId] = draft
}
}
fun updateFontSize() {
fontSize = activity.getTextSize()
notifyDataSetChanged()
}
fun updateConversations(newConversations: ArrayList<Conversation>, commitCallback: (() -> Unit)? = null) {
saveRecyclerViewState()
submitList(newConversations.toList(), commitCallback)
}
fun updateDrafts() {
ensureBackgroundThread {
val newDrafts = HashMap<Long, String?>()
fetchDrafts(newDrafts)
if (drafts.hashCode() != newDrafts.hashCode()) {
drafts = newDrafts
activity.runOnUiThread {
notifyDataSetChanged()
}
}
}
}
private fun setupView(view: View, conversation: Conversation) {
view.apply {
setupViewBackground(activity)
val smsDraft = drafts[conversation.threadId]
draft_indicator.beVisibleIf(smsDraft != null)
draft_indicator.setTextColor(properPrimaryColor)
pin_indicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString()))
pin_indicator.applyColorFilter(textColor)
conversation_frame.isSelected = selectedKeys.contains(conversation.hashCode())
conversation_address.apply {
text = conversation.title
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
}
conversation_body_short.apply {
text = smsDraft ?: conversation.snippet
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f)
}
conversation_date.apply {
text = conversation.date.formatDateOrTime(context, true, false)
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
}
val style = if (conversation.read) {
conversation_body_short.alpha = 0.7f
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
} else {
conversation_body_short.alpha = 1f
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
}
conversation_address.setTypeface(null, style)
conversation_body_short.setTypeface(null, style)
arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
it.setTextColor(textColor)
}
// at group conversations we use an icon as the placeholder, not any letter
val placeholder = if (conversation.isGroupConversation) {
SimpleContactsHelper(context).getColoredGroupIcon(conversation.title)
} else {
null
}
SimpleContactsHelper(context).loadContactImage(conversation.photoUri, conversation_image, conversation.title, placeholder)
}
}
override fun onChange(position: Int) = currentList.getOrNull(position)?.title ?: ""
private fun refreshConversations() { private fun refreshConversations() {
activity.runOnUiThread { activity.runOnUiThread {
refreshMessages() refreshMessages()
finishActMode() finishActMode()
} }
} }
private fun saveRecyclerViewState() {
recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
}
private fun restoreRecyclerViewState() {
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
}
private class ConversationDiffCallback : DiffUtil.ItemCallback<Conversation>() {
override fun areItemsTheSame(oldItem: Conversation, newItem: Conversation): Boolean {
return Conversation.areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(oldItem: Conversation, newItem: Conversation): Boolean {
return Conversation.areContentsTheSame(oldItem, newItem)
}
}
} }

View File

@ -12,12 +12,9 @@ import com.simplemobiletools.smsmessenger.interfaces.AttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao
import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.MessagesDao import com.simplemobiletools.smsmessenger.interfaces.MessagesDao
import com.simplemobiletools.smsmessenger.models.Attachment import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.models.MessageAttachment
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 7) @Database(entities = [Conversation::class, ArchivedConversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 8)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class MessagesDatabase : RoomDatabase() { abstract class MessagesDatabase : RoomDatabase() {
@ -44,6 +41,7 @@ abstract class MessagesDatabase : RoomDatabase() {
.addMigrations(MIGRATION_4_5) .addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6) .addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7) .addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.build() .build()
} }
} }
@ -115,5 +113,13 @@ abstract class MessagesDatabase : RoomDatabase() {
} }
} }
} }
private val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("CREATE TABLE archived_conversations (`thread_id` INTEGER NOT NULL PRIMARY KEY, `deleted_ts` INTEGER NOT NULL)")
}
}
}
} }
} }

View File

@ -0,0 +1,39 @@
package com.simplemobiletools.smsmessenger.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.beGoneIf
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.smsmessenger.R
import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.delete_remember_title
import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.skip_the_recycle_bin_checkbox
class DeleteConfirmationDialog(
private val activity: Activity,
private val message: String,
private val showSkipRecycleBinOption: Boolean,
private val callback: (skipRecycleBin: Boolean) -> Unit
) {
private var dialog: AlertDialog? = null
val view = activity.layoutInflater.inflate(R.layout.dialog_delete_confirmation, null)!!
init {
view.delete_remember_title.text = message
view.skip_the_recycle_bin_checkbox.beGoneIf(!showSkipRecycleBinOption)
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.yes) { _, _ -> dialogConfirmed() }
.setNegativeButton(R.string.no, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
}
}
}
private fun dialogConfirmed() {
dialog?.dismiss()
callback(view.skip_the_recycle_bin_checkbox.isChecked)
}
}

View File

@ -581,6 +581,35 @@ fun Context.insertNewSMS(address: String, subject: String, body: String, date: L
} }
} }
fun Context.checkAndDeleteOldArchivedConversations(callback: (() -> Unit)? = null) {
if (config.useArchive && config.lastArchiveCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) {
config.lastArchiveCheck = System.currentTimeMillis()
ensureBackgroundThread {
try {
for (conversation in conversationsDB.getOldArchived(System.currentTimeMillis() - MONTH_SECONDS * 1000L)) {
deleteConversation(conversation.threadId)
}
callback?.invoke()
} catch (e: Exception) {
}
}
}
}
fun Context.removeAllArchivedConversations(callback: (() -> Unit)? = null) {
ensureBackgroundThread {
try {
for (conversation in conversationsDB.getAllArchived()) {
deleteConversation(conversation.threadId)
}
toast(R.string.recycle_bin_emptied)
callback?.invoke()
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
}
fun Context.deleteConversation(threadId: Long) { fun Context.deleteConversation(threadId: Long) {
var uri = Sms.CONTENT_URI var uri = Sms.CONTENT_URI
val selection = "${Sms.THREAD_ID} = ?" val selection = "${Sms.THREAD_ID} = ?"
@ -602,6 +631,15 @@ fun Context.deleteConversation(threadId: Long) {
messagesDB.deleteThreadMessages(threadId) messagesDB.deleteThreadMessages(threadId)
} }
fun Context.moveConversationToRecycleBin(threadId: Long) {
conversationsDB.archiveConversation(
ArchivedConversation(
threadId = threadId,
deletedTs = System.currentTimeMillis()
)
)
}
fun Context.deleteMessage(id: Long, isMMS: Boolean) { fun Context.deleteMessage(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val selection = "${Sms._ID} = ?" val selection = "${Sms._ID} = ?"

View File

@ -91,4 +91,12 @@ class Config(context: Context) : BaseConfig(context) {
var keyboardHeight: Int var keyboardHeight: Int
get() = prefs.getInt(SOFT_KEYBOARD_HEIGHT, context.getDefaultKeyboardHeight()) get() = prefs.getInt(SOFT_KEYBOARD_HEIGHT, context.getDefaultKeyboardHeight())
set(keyboardHeight) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, keyboardHeight).apply() set(keyboardHeight) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, keyboardHeight).apply()
var useArchive: Boolean
get() = prefs.getBoolean(USE_ARCHIVE, true)
set(useArchive) = prefs.edit().putBoolean(USE_ARCHIVE, useArchive).apply()
var lastArchiveCheck: Long
get() = prefs.getLong(LAST_ARCHIVE_CHECK, 0L)
set(lastArchiveCheck) = prefs.edit().putLong(LAST_ARCHIVE_CHECK, lastArchiveCheck).apply()
} }

View File

@ -37,6 +37,8 @@ const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height" const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height"
const val IS_MMS = "is_mms" const val IS_MMS = "is_mms"
const val MESSAGE_ID = "message_id" const val MESSAGE_ID = "message_id"
const val USE_ARCHIVE = "use_recycle_bin"
const val LAST_ARCHIVE_CHECK = "last_bin_check"
private const val PATH = "com.simplemobiletools.smsmessenger.action." private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read" const val MARK_AS_READ = PATH + "mark_as_read"

View File

@ -1,9 +1,7 @@
package com.simplemobiletools.smsmessenger.interfaces package com.simplemobiletools.smsmessenger.interfaces
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert import com.simplemobiletools.smsmessenger.models.ArchivedConversation
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.simplemobiletools.smsmessenger.models.Conversation import com.simplemobiletools.smsmessenger.models.Conversation
@Dao @Dao
@ -11,8 +9,20 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdate(conversation: Conversation): Long fun insertOrUpdate(conversation: Conversation): Long
@Query("SELECT * FROM conversations") @Query("SELECT conversations.* FROM conversations LEFT OUTER JOIN archived_conversations ON conversations.thread_id = archived_conversations.thread_id WHERE archived_conversations.deleted_ts is NULL")
fun getAll(): List<Conversation> fun getNonArchived(): List<Conversation>
@Query("SELECT conversations.* FROM archived_conversations INNER JOIN conversations ON conversations.thread_id = archived_conversations.thread_id")
fun getAllArchived(): List<Conversation>
@Query("SELECT COUNT(*) FROM archived_conversations")
fun getArchivedCount(): Int
@Query("SELECT * FROM archived_conversations WHERE deleted_ts < :timestamp")
fun getOldArchived(timestamp: Long): List<ArchivedConversation>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun archiveConversation(archivedConversation: ArchivedConversation)
@Query("SELECT * FROM conversations WHERE thread_id = :threadId") @Query("SELECT * FROM conversations WHERE thread_id = :threadId")
fun getConversationWithThreadId(threadId: Long): Conversation? fun getConversationWithThreadId(threadId: Long): Conversation?
@ -30,5 +40,14 @@ interface ConversationsDao {
fun markUnread(threadId: Long) fun markUnread(threadId: Long)
@Query("DELETE FROM conversations WHERE thread_id = :threadId") @Query("DELETE FROM conversations WHERE thread_id = :threadId")
fun deleteThreadId(threadId: Long) fun deleteThreadFromConversations(threadId: Long)
@Query("DELETE FROM archived_conversations WHERE thread_id = :threadId")
fun deleteThreadFromArchivedConversations(threadId: Long)
@Transaction
fun deleteThreadId(threadId: Long) {
deleteThreadFromConversations(threadId)
deleteThreadFromArchivedConversations(threadId)
}
} }

View File

@ -0,0 +1,15 @@
package com.simplemobiletools.smsmessenger.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "archived_conversations",
indices = [(Index(value = ["thread_id"], unique = true))]
)
data class ArchivedConversation(
@PrimaryKey @ColumnInfo(name = "thread_id") var threadId: Long,
@ColumnInfo(name = "deleted_ts") var deletedTs: Long
)

View File

@ -100,6 +100,7 @@ class SmsReceiver : BroadcastReceiver() {
subscriptionId subscriptionId
) )
context.messagesDB.insertOrUpdate(message) context.messagesDB.insertOrUpdate(message)
context.conversationsDB.deleteThreadFromArchivedConversations(threadId)
refreshMessages() refreshMessages()
context.showReceivedMessageNotification(newMessageId, address, body, threadId, bitmap) context.showReceivedMessageNotification(newMessageId, address, body, threadId, bitmap)
} }

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/archive_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/archive_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/color_primary"
app:title="Archived messages"
app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle" />
<RelativeLayout
android:id="@+id/archive_nested_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:fillViewport="true"
android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/archive_coordinator_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/archive_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/conversations_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward"
app:showAnimationBehavior="inward"
app:showDelay="250"
tools:visibility="visible" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/no_conversations_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/bigger_margin"
android:alpha="0.8"
android:gravity="center"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin"
android:text="@string/no_conversations_found"
android:textSize="@dimen/bigger_text_size"
android:textStyle="italic"
android:visibility="gone" />
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:id="@+id/conversations_fastscroller"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyRecyclerView
android:id="@+id/conversations_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:layoutAnimation="@anim/layout_animation"
android:overScrollMode="ifContentScrolls"
android:scrollbars="none"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager" />
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -346,6 +346,55 @@
android:id="@+id/settings_outgoing_messages_divider" android:id="@+id/settings_outgoing_messages_divider"
layout="@layout/divider" /> layout="@layout/divider" />
<TextView
android:id="@+id/settings_recycle_bin_label"
style="@style/SettingsSectionLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/recycle_bin" />
<RelativeLayout
android:id="@+id/settings_use_recycle_bin_holder"
style="@style/SettingsHolderCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/settings_use_recycle_bin"
style="@style/SettingsCheckboxStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/move_items_into_recycle_bin" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/settings_empty_recycle_bin_holder"
style="@style/SettingsHolderTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_label"
style="@style/SettingsTextLabelStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_recycle_bin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_empty_recycle_bin_size"
style="@style/SettingsTextValueStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/settings_empty_recycle_bin_label"
tools:text="0 B" />
</RelativeLayout>
<include
android:id="@+id/settings_recycle_bin_divider"
layout="@layout/divider" />
<TextView <TextView
android:id="@+id/settings_security_label" android:id="@+id/settings_security_label"
style="@style/SettingsSectionLabelStyle" style="@style/SettingsSectionLabelStyle"

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/delete_remember_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/big_margin"
android:paddingTop="@dimen/big_margin"
android:paddingRight="@dimen/big_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/delete_remember_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingBottom="@dimen/activity_margin"
android:text="@string/delete_whole_conversation_confirmation"
android:textSize="@dimen/bigger_text_size" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/skip_the_recycle_bin_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/delete_remember_title"
android:text="@string/skip_the_recycle_bin" />
</RelativeLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource,AlwaysShowAction">
<item
android:id="@+id/empty_archive"
android:showAsAction="never"
android:title="Empty archive messages"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource,AlwaysShowAction">
<item
android:id="@+id/cab_delete"
android:icon="@drawable/ic_delete_vector"
android:showAsAction="always"
android:title="@string/delete"
app:showAsAction="always" />
<item
android:id="@+id/cab_select_all"
android:icon="@drawable/ic_select_all_vector"
android:title="@string/select_all"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cab_unarchive"
android:showAsAction="never"
android:title="Unarchive"
app:showAsAction="never" />
</menu>

View File

@ -13,6 +13,11 @@
android:showAsAction="never" android:showAsAction="never"
android:title="@string/export_messages" android:title="@string/export_messages"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/show_archived"
android:showAsAction="never"
android:title="Show archived messages"
app:showAsAction="never" />
<item <item
android:id="@+id/settings" android:id="@+id/settings"
android:icon="@drawable/ic_settings_cog_vector" android:icon="@drawable/ic_settings_cog_vector"