From 458be75114e8c7c5ac6ebcae3bb10ed53e3cff5b Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Wed, 5 May 2021 20:54:25 +0200 Subject: [PATCH] Batch contact delete --- .../java/sushi/hardcore/aira/ChatActivity.kt | 8 +- .../java/sushi/hardcore/aira/MainActivity.kt | 93 +++++++++++++++++-- .../hardcore/aira/adapters/ChatAdapter.kt | 10 +- .../hardcore/aira/adapters/SessionAdapter.kt | 27 +++++- .../aira/background_service/AIRAService.kt | 3 +- .../sushi/hardcore/aira/utils/StringUtils.kt | 4 + .../main/res/drawable/background_session.xml | 8 -- app/src/main/res/layout/activity_chat.xml | 3 +- app/src/main/res/layout/adapter_session.xml | 3 +- app/src/main/res/menu/main_activity.xml | 13 ++- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 3 + 12 files changed, 146 insertions(+), 30 deletions(-) delete mode 100644 app/src/main/res/drawable/background_session.xml diff --git a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt index d7f7e3a..59d1fbc 100644 --- a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt +++ b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt @@ -1,6 +1,9 @@ package sushi.hardcore.aira -import android.content.* +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import android.provider.OpenableColumns @@ -12,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import sushi.hardcore.aira.adapters.ChatAdapter @@ -24,7 +28,6 @@ import sushi.hardcore.aira.databinding.DialogInfoBinding import sushi.hardcore.aira.utils.FileUtils import sushi.hardcore.aira.utils.StringUtils import java.io.FileNotFoundException -import java.util.* class ChatActivity : AppCompatActivity() { private external fun generateFingerprint(publicKey: ByteArray): String @@ -185,6 +188,7 @@ class ChatActivity : AppCompatActivity() { airaService.isAppInBackground = false if (airaService.isOnline(sessionId)) { onConnected() + binding.recyclerChat.updatePadding(bottom = 0) } airaService.setSeen(sessionId, true) } diff --git a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt index 370c869..e83379a 100644 --- a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt @@ -32,9 +32,6 @@ class MainActivity : AppCompatActivity() { private lateinit var airaService: AIRAService private lateinit var onlineSessionAdapter: SessionAdapter private var offlineSessionAdapter: SessionAdapter? = null - private val onSessionsItemClick = AdapterView.OnItemClickListener { adapter, _, position, _ -> - launchChatActivity(adapter.getItemAtPosition(position) as Session) - } private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ -> askShareFileTo(adapter.getItemAtPosition(position) as Session) } @@ -97,8 +94,18 @@ class MainActivity : AppCompatActivity() { onItemClickListener = if (openedToShareFile) { onSessionsItemClickSendFile } else { - onSessionsItemClick + AdapterView.OnItemClickListener { _, _, position, _ -> + if (isSelecting()) { + changeSelection(onlineSessionAdapter, position) + } else { + launchChatActivity(onlineSessionAdapter.getItem(position)) + } + } } + onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ -> + changeSelection(onlineSessionAdapter, position) + true + } setOnScrollListener(onSessionsScrollListener) } if (openedToShareFile) { @@ -111,7 +118,17 @@ class MainActivity : AppCompatActivity() { onItemClickListener = if (openedToShareFile) { onSessionsItemClickSendFile } else { - onSessionsItemClick + AdapterView.OnItemClickListener { _, _, position, _ -> + if (isSelecting()) { + changeSelection(offlineSessionAdapter!!, position) + } else { + launchChatActivity(offlineSessionAdapter!!.getItem(position)) + } + } + } + onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ -> + changeSelection(offlineSessionAdapter!!, position) + true } setOnScrollListener(onSessionsScrollListener) } @@ -166,8 +183,9 @@ class MainActivity : AppCompatActivity() { } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main_activity, menu) + menu.findItem(R.id.remove_contact).isVisible = isSelecting() return true } @@ -194,6 +212,30 @@ class MainActivity : AppCompatActivity() { } true } + R.id.remove_contact -> { + AlertDialog.Builder(this) + .setTitle(R.string.warning) + .setMessage(R.string.ask_remove_contacts) + .setPositiveButton(R.string.delete) { _, _ -> + Thread { + for (sessionId in onlineSessionAdapter.getSelectedSessionIds()) { + airaService.removeContact(sessionId) + } + offlineSessionAdapter?.let { + for (sessionId in it.getSelectedSessionIds()) { + airaService.removeContact(sessionId) + } + } + runOnUiThread { + unSelectAll() + refreshSessions() + } + }.start() + } + .setNegativeButton(R.string.cancel, null) + .show() + true + } else -> super.onOptionsItemSelected(item) } } @@ -211,10 +253,7 @@ class MainActivity : AppCompatActivity() { if (AIRAService.isServiceRunning) { airaService.isAppInBackground = false airaService.uiCallbacks = uiCallbacks //restoring callbacks - onlineSessionAdapter.reset() - offlineSessionAdapter?.reset() - loadContacts() - loadSessions() + refreshSessions() title = airaService.identityName } else { finish() @@ -222,6 +261,40 @@ class MainActivity : AppCompatActivity() { } } + override fun onBackPressed() { + if (isSelecting()) { + unSelectAll() + } else { + super.onBackPressed() + } + } + + private fun refreshSessions() { + onlineSessionAdapter.reset() + offlineSessionAdapter?.reset() + loadContacts() + loadSessions() + } + + private fun unSelectAll() { + onlineSessionAdapter.unSelectAll() + offlineSessionAdapter?.unSelectAll() + invalidateOptionsMenu() + } + + private fun changeSelection(adapter: SessionAdapter, position: Int) { + val wasSelecting = adapter.selectedItems.isNotEmpty() + adapter.onSelectionChanged(position) + val isSelecting = adapter.selectedItems.isNotEmpty() + if (wasSelecting != isSelecting) { + invalidateOptionsMenu() + } + } + + private fun isSelecting(): Boolean { + return onlineSessionAdapter.selectedItems.isNotEmpty() || offlineSessionAdapter?.selectedItems?.isNotEmpty() == true + } + private fun loadContacts() { if (offlineSessionAdapter != null) { for ((sessionId, contact) in airaService.contacts) { diff --git a/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt b/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt index d65a242..cc6bb1f 100644 --- a/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt +++ b/app/src/main/java/sushi/hardcore/aira/adapters/ChatAdapter.kt @@ -11,6 +11,8 @@ import android.widget.ImageButton import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.marginEnd +import androidx.core.view.updatePadding import androidx.recyclerview.widget.RecyclerView import sushi.hardcore.aira.ChatItem import sushi.hardcore.aira.R @@ -44,9 +46,11 @@ class ChatAdapter( internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) { fun handleItemView(position: Int) { - if (position == 0) { - itemView.setPadding(itemView.paddingLeft, 50, itemView.paddingRight, itemView.paddingBottom) - } + itemView.updatePadding(top = if (position == 0) { + 50 + } else { + itemView.paddingBottom + }) } fun setBubbleColor(bubble: View, outgoing: Boolean) { if (outgoing) { diff --git a/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt b/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt index c719c54..16e17d9 100644 --- a/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt +++ b/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt @@ -8,12 +8,14 @@ import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import sushi.hardcore.aira.R import sushi.hardcore.aira.widgets.TextAvatar -class SessionAdapter(context: Context): BaseAdapter() { +class SessionAdapter(val context: Context): BaseAdapter() { private val sessions = mutableListOf() private val inflater: LayoutInflater = LayoutInflater.from(context) + val selectedItems = mutableListOf() override fun getCount(): Int { return sessions.size @@ -55,6 +57,11 @@ class SessionAdapter(context: Context): BaseAdapter() { } else { View.VISIBLE } + view.setBackgroundColor(ContextCompat.getColor(context, if (selectedItems.contains(position)) { + R.color.itemSelected + } else { + R.color.sessionBackground + })) return view } @@ -99,4 +106,22 @@ class SessionAdapter(context: Context): BaseAdapter() { sessions.clear() notifyDataSetChanged() } + + fun onSelectionChanged(position: Int) { + if (!selectedItems.contains(position)) { + selectedItems.add(position) + } else { + selectedItems.remove(position) + } + notifyDataSetChanged() + } + + fun unSelectAll() { + selectedItems.clear() + notifyDataSetChanged() + } + + fun getSelectedSessionIds(): List { + return selectedItems.map { position -> sessions[position].sessionId } + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt index 420963b..d28ebc2 100644 --- a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt +++ b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt @@ -14,6 +14,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import sushi.hardcore.aira.* +import sushi.hardcore.aira.utils.StringUtils import java.io.IOException import java.io.InputStream import java.net.* @@ -556,7 +557,7 @@ class AIRAService : Service() { } } Protocol.TELL_NAME -> { - val name = buffer.sliceArray(1 until buffer.size).decodeToString() + val name = StringUtils.sanitizeName(buffer.sliceArray(1 until buffer.size).decodeToString()) uiCallbacks?.onNameTold(sessionId, name) val contact = contacts[sessionId] if (contact == null) { diff --git a/app/src/main/java/sushi/hardcore/aira/utils/StringUtils.kt b/app/src/main/java/sushi/hardcore/aira/utils/StringUtils.kt index 03a9251..453e254 100644 --- a/app/src/main/java/sushi/hardcore/aira/utils/StringUtils.kt +++ b/app/src/main/java/sushi/hardcore/aira/utils/StringUtils.kt @@ -21,4 +21,8 @@ object StringUtils { rawIp.substring(0, i) } } + + fun sanitizeName(name: String): String { + return name.replace('\n', ' ') + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/background_session.xml b/app/src/main/res/drawable/background_session.xml deleted file mode 100644 index d70974f..0000000 --- a/app/src/main/res/drawable/background_session.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 9d064dd..24154f8 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -10,7 +10,8 @@ android:id="@+id/recycler_chat" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginHorizontal="20dp" + android:paddingHorizontal="20dp" + android:paddingBottom="20dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/bottom_panel"/> diff --git a/app/src/main/res/layout/adapter_session.xml b/app/src/main/res/layout/adapter_session.xml index 6ec7bf2..9b61fc2 100644 --- a/app/src/main/res/layout/adapter_session.xml +++ b/app/src/main/res/layout/adapter_session.xml @@ -4,8 +4,7 @@ android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="10dp" - android:background="@drawable/background_session"> + android:padding="10dp"> + android:icon="@drawable/ic_settings" + android:title="@string/settings" /> + android:icon="@drawable/ic_close" + android:title="@string/log_out"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index dce286a..1f6520a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,4 +9,5 @@ @color/secondary #3845A3 #ffffff + #66666666 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1468a70..8abd17f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ Deleting a conversation only affects you. Your contact will still have a copy of this conversation if she/he doesn\'t delete it too. Do you really want to delete all this conversation (messages and files) ? Delete Deleting contact will remove her/his identity key and your conversation (messages and files). You won\'t be able to recognize her/him anymore. This action only affects you. Do you really want to remove this contact ? + Deleting contacts will remove their identity keys and your conversations (messages and files). You won\'t be able to recognize them anymore. This action only affects you. Do you really want to remove these contacts ? Encrypt with a password New Messages Mark read @@ -72,4 +73,6 @@ Details Your IP addresses: Another file transfer is already in progress + Settings + Log out