🎉 Separate messages by identity

Also, allow deleting all messages/conversations in a list
This commit is contained in:
Christian Basler 2018-08-24 17:32:29 +02:00
parent 9f2508c1a5
commit a9602368fb
14 changed files with 218 additions and 71 deletions

View File

@ -85,10 +85,10 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onResume()
initFab(activity as MainActivity)
updateList()
reloadList()
}
fun updateList() {
override fun reloadList() {
adapter.clear()
context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context)
@ -138,7 +138,7 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
}
}
override fun updateList(label: Void) = updateList()
override fun updateList(label: Void) = reloadList()
private data class ViewHolder(
val ctx: Context,

View File

@ -33,6 +33,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.ConversationService
@ -43,7 +44,9 @@ import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchAct
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.cancelButton
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.alert
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import java.util.*
@ -90,6 +93,7 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var conversationService: ConversationService
private var activateOnItemClick: Boolean = false
@ -103,7 +107,8 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
val conversationIds = messageRepo.findConversations(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE
PAGE_SIZE,
context?.preferences?.separateIdentities == true
)
conversationIds.forEach { conversationId ->
val conversation = conversationService.getConversation(conversationId)
@ -139,6 +144,8 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
val mainActivity = activity as? MainActivity
swipeableConversationAdapter?.clear(label)
@ -148,6 +155,9 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
return
}
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
deleteAllMenuItem?.isVisible = false
mainActivity?.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
@ -298,6 +308,7 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
super.onCreateOptionsMenu(menu, inflater)
}
@ -307,13 +318,21 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
deleteAllMessages(label)
}
uiThread { doUpdateList(label) }
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
alert(
title = R.string.delete_all_messages_in_list,
message = R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
return true
}
@ -321,6 +340,16 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
}
}
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.value = label
}

View File

@ -20,6 +20,8 @@ package ch.dissem.apps.abit
* @author Christian Basler
*/
interface ListHolder<in L> {
fun reloadList()
fun updateList(label: L)
fun setActivateOnItemClick(activateOnItemClick: Boolean)

View File

@ -29,6 +29,7 @@ import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.*
@ -92,6 +93,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private set
private lateinit var bmc: BitmessageContext
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer
@ -104,6 +106,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
super.onCreate(savedInstanceState)
instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this)
messageRepo = Singleton.getMessageRepository(this)
setContentView(R.layout.activity_main)
fab.hide()
@ -299,8 +302,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
for (label in labels) {
addLabelEntry(label)
}
currentLabel.value?.let {
drawer.setSelection(it.id as Long)
currentLabel.value?.let { label ->
drawer.setSelection(label.id as Long)
}
updateUnread()
}
@ -334,7 +337,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
when (item.name.textRes) {
R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) {
itemList.updateList()
itemList.reloadList()
} else {
changeList(AddressListFragment())
}
@ -432,7 +435,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
if (item.tag is Label) {
val label = item.tag as Label
if (label !== LABEL_ARCHIVE) {
val unread = bmc.messages.countUnread(label)
val unread = messageRepo.countUnread(label, preferences.separateIdentities)
if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString())
} else {

View File

@ -33,6 +33,7 @@ import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
@ -42,9 +43,9 @@ import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchAct
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.*
import org.jetbrains.anko.support.v4.alert
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import java.util.*
private const val PAGE_SIZE = 15
@ -89,6 +90,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false
@ -98,10 +100,12 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
isLoading = true
swipeableMessageAdapter?.let { messageAdapter ->
doAsync {
val label = currentLabel.value
val messages = messageRepo.findMessages(
currentLabel.value,
label,
messageAdapter.itemCount,
PAGE_SIZE
PAGE_SIZE,
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
)
onUiThread {
messageAdapter.addAll(messages)
@ -133,6 +137,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
// If the menu item isn't available yet, we should wait - the method will be called again once it's
// initialized.
@ -155,6 +161,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems()
}
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
}
override fun onCreateView(
@ -300,6 +307,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
currentLabel.value?.let { doUpdateList(it) }
super.onCreateOptionsMenu(menu, inflater)
}
@ -310,13 +318,21 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
deleteAllMessages(label)
}
uiThread { doUpdateList(label) }
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
alert(
title = R.string.delete_all_messages_in_list,
message = R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
return true
}
@ -324,6 +340,16 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
}
}
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.value = label
}

View File

@ -42,6 +42,13 @@ class ProfileSelectionListener(
val tag = profile.tag
if (tag is BitmessageAddress) {
Singleton.setIdentity(tag)
MainActivity.apply {
updateUnread()
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
if (itemList is ListHolder<*>) {
itemList.reloadList()
}
}
}
}
}

View File

@ -21,6 +21,7 @@ import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress
@ -38,7 +39,14 @@ import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
if (label === LABEL_ARCHIVE || label === null) {
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
} else {
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
}
override fun findMessages(label: Label?, offset: Int, limit: Int) =
if (label === LABEL_ARCHIVE) {
@ -54,30 +62,56 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
null
).toInt()
override fun countUnread(label: Label?) = when {
private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
if (separateIdentities) {
val identity = prefs.currentIdentity
return if (prefs.separateIdentities && identity != null) {
"AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
} else {
"" to emptyArray()
}
} else {
return "" to emptyArray()
}
}
override fun countUnread(label: Label?) = countUnread(label, false)
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
when {
label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(Label.Type.UNREAD.name)
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
}
}
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> {
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
label == null -> null
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
label == null -> if (selectIdentityQuery.isNotBlank()) {
"type = 'BROADCAST' OR recipient=? OR sender=?"
} else {
null
}
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
}
val result = LinkedList<UUID>()
sql.readableDatabase.query(
@ -85,7 +119,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
TABLE_NAME,
projection,
where,
null, null, null,
selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
@ -140,8 +174,11 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
db.update(PARENTS_TABLE_NAME, values, where, null)
}
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> {
override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
val result = LinkedList<Plaintext>()
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
// Define a projection that specifies which columns from the database
// you will actually use after this query.
@ -164,7 +201,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
"$where $selectIdentityQuery", selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
@ -318,4 +355,5 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id"
}
}

View File

@ -110,12 +110,13 @@ object Singleton {
inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper)
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper)
messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx)
labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false
preferences.port = context.preferences.listeningPort
}
}

View File

@ -29,6 +29,7 @@ object Constants {
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
const val PREFERENCE_POW_COUNT = "pow_count"
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
const val BITMESSAGE_URL_SCHEMA = "bitmessage:"

View File

@ -21,10 +21,12 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS
import ch.dissem.apps.abit.util.Constants.PREFERENCE_ONLINE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SEPARATE_IDENTITIES
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager
@ -48,6 +50,8 @@ class Preferences internal constructor(private val ctx: Context) {
private val isAllowedForCharging get() = !requireCharging || isCharging
private val sharedPreferences = ctx.defaultSharedPreferences
private val isCharging
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ctx.batteryManager.isCharging
@ -58,20 +62,20 @@ class Preferences internal constructor(private val ctx: Context) {
}
var wifiOnly
get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
get() = sharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
set(value) {
ctx.defaultSharedPreferences.edit()
sharedPreferences.edit()
.putBoolean(PREFERENCE_WIFI_ONLY, value)
.apply()
}
val requireCharging get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
val requireCharging get() = sharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
val emulateConversations get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
val emulateConversations get() = sharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
val exportDirectory by lazy { File(ctx.filesDir, "exports") }
val requestAcknowledgements = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
val requestAcknowledgements = sharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
fun cleanupExportDirectory() {
if (exportDirectory.exists()) {
@ -88,9 +92,9 @@ class Preferences internal constructor(private val ctx: Context) {
}
var online
get() = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_ONLINE, true)
get() = sharedPreferences.getBoolean(PREFERENCE_ONLINE, true)
set(value) {
ctx.defaultSharedPreferences.edit()
sharedPreferences.edit()
.putBoolean(PREFERENCE_ONLINE, value)
.apply()
if (value) {
@ -100,6 +104,16 @@ class Preferences internal constructor(private val ctx: Context) {
}
}
val separateIdentities
get() = sharedPreferences.getBoolean(PREFERENCE_SEPARATE_IDENTITIES, false)
val currentIdentity
get() = Singleton.getIdentity(ctx)
val listeningPort
get() = sharedPreferences.getString("listening_port", null)?.toIntOrNull()
?: 8444
companion object {
private var instance: WeakReference<Preferences>? = null

View File

@ -4,8 +4,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/empty_trash"
app:showAsAction="never"
android:icon="@drawable/ic_action_delete"
android:title="@string/empty_trash"
android:visible="false"/>
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/delete_all"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete_all_messages_in_list"
android:visible="true"
app:showAsAction="never" />
</menu>

View File

@ -145,4 +145,5 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
<string name="preference_group_user_experience">Verhalten</string>
<string name="preference_group_user_experience_summary">Ändern, wie Nachrichten dargestellt werden</string>
<string name="bitmessage_service_description">Hält die Verbindung zum Bitmessage-Netzwerk.</string>
<string name="preference_port">Port</string>
</resources>

View File

@ -156,4 +156,10 @@ As an alternative you could configure a trusted node in the settings, but as of
<string name="online">Online</string>
<string name="warning_low_memory">Low memory!</string>
<string name="bitmessage_service_description">Keeps the connection to the bitmessage network.</string>
<string name="preference_port">Port</string>
<string name="preference_port_summary">Listen on this port for incoming connections. You might need to shortly go offline before the new port is used.</string>
<string name="preference_separate_identities_summary">Show messages for selected identity only</string>
<string name="preference_separate_identities">Filter messages by identity</string>
<string name="delete_all_messages_in_list">Delete all</string>
<string name="delete_all_messages_in_list_ask">Delete all messages in list?</string>
</resources>

View File

@ -3,10 +3,15 @@
<PreferenceScreen
android:key="preference_ux"
android:title="@string/preference_group_user_experience"
android:persistent="false"
android:summary="@string/preference_group_user_experience_summary"
android:persistent="false">
android:title="@string/preference_group_user_experience">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="separate_identities"
android:summary="@string/preference_separate_identities_summary"
android:title="@string/preference_separate_identities" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="emulate_conversations"
@ -22,9 +27,9 @@
<PreferenceScreen
android:key="preference_network_and_performance"
android:title="@string/preference_group_network_and_performance"
android:persistent="false"
android:summary="@string/preference_group_network_and_performance_summary"
android:persistent="false">
android:title="@string/preference_group_network_and_performance">
<SwitchPreferenceCompat
android:defaultValue="true"
@ -46,9 +51,9 @@
<PreferenceScreen
android:key="preference_advanced"
android:title="@string/preference_group_advanced"
android:persistent="false"
android:summary="@string/preference_group_advanced_summary"
android:persistent="false">
android:title="@string/preference_group_advanced">
<Preference
android:key="cleanup"
@ -68,6 +73,13 @@
android:summary="@string/status_summary"
android:title="@string/status" />
<EditTextPreference
android:defaultValue="8444"
android:key="listening_port"
android:summary="@string/preference_port_summary"
android:title="@string/preference_port"
android:numeric="integer"/>
</PreferenceScreen>
<Preference