session-android/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt

399 lines
19 KiB
Kotlin
Raw Normal View History

2020-05-11 08:19:26 +02:00
package org.thoughtcrime.securesms.loki.activities
2019-12-17 14:27:59 +01:00
2020-02-19 05:34:02 +01:00
import android.app.AlertDialog
2019-12-19 11:15:58 +01:00
import android.arch.lifecycle.Observer
2020-07-16 04:49:37 +02:00
import android.content.BroadcastReceiver
import android.content.Context
2019-12-17 15:15:13 +01:00
import android.content.Intent
2020-07-16 04:49:37 +02:00
import android.content.IntentFilter
2019-12-19 11:49:23 +01:00
import android.database.Cursor
2020-07-30 08:53:34 +02:00
import android.net.Uri
2020-01-07 06:44:53 +01:00
import android.os.AsyncTask
2019-12-17 14:27:59 +01:00
import android.os.Bundle
import android.os.Handler
2019-12-19 11:49:23 +01:00
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
2020-07-16 04:49:37 +02:00
import android.support.v4.content.LocalBroadcastManager
2019-12-17 14:27:59 +01:00
import android.support.v7.widget.LinearLayoutManager
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
2020-03-16 05:35:14 +01:00
import android.util.DisplayMetrics
import android.view.View
2020-03-16 05:35:14 +01:00
import android.widget.RelativeLayout
2020-02-19 05:34:02 +01:00
import android.widget.Toast
2019-12-17 14:27:59 +01:00
import kotlinx.android.synthetic.main.activity_home.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
2019-12-17 15:15:13 +01:00
import org.thoughtcrime.securesms.conversation.ConversationActivity
2019-12-17 14:27:59 +01:00
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
2019-12-17 15:15:13 +01:00
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
2020-07-30 08:53:34 +02:00
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
2020-05-11 08:54:31 +02:00
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
2020-07-15 06:26:20 +02:00
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
2020-06-02 07:18:09 +02:00
import org.thoughtcrime.securesms.loki.utilities.*
2020-05-11 08:19:26 +02:00
import org.thoughtcrime.securesms.loki.views.ConversationView
import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
2019-12-19 11:15:58 +01:00
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
2020-08-11 04:20:17 +02:00
import org.thoughtcrime.securesms.util.GroupUtil
2019-12-17 14:27:59 +01:00
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
2020-07-15 04:24:43 +02:00
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
2020-05-14 05:52:20 +02:00
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
2020-08-04 07:33:37 +02:00
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
2020-05-14 05:52:20 +02:00
import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol
2020-08-04 07:33:37 +02:00
import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol
2020-08-11 04:20:17 +02:00
import org.whispersystems.signalservice.loki.utilities.toHexString
2019-12-17 14:27:59 +01:00
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
2019-12-19 11:15:58 +01:00
private lateinit var glide: GlideRequests
2020-07-16 04:49:37 +02:00
private var broadcastReceiver: BroadcastReceiver? = null
2019-12-17 14:27:59 +01:00
2020-07-15 06:26:20 +02:00
private val publicKey: String
2020-01-06 04:26:52 +01:00
get() {
2020-07-15 06:26:20 +02:00
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
return masterPublicKey ?: userPublicKey
2020-01-06 04:26:52 +01:00
}
// region Lifecycle
2019-12-17 14:27:59 +01:00
constructor() : super()
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
// Process any outstanding deletes
val threadDatabase = DatabaseFactory.getThreadDatabase(this)
val archivedConversationCount = threadDatabase.archivedConversationListCount
if (archivedConversationCount > 0) {
val archivedConversations = threadDatabase.archivedConversationList
archivedConversations.moveToFirst()
fun deleteThreadAtCurrentPosition() {
val threadID = archivedConversations.getLong(archivedConversations.getColumnIndex(ThreadDatabase.ID))
AsyncTask.execute {
threadDatabase.deleteConversation(threadID)
(applicationContext as ApplicationContext).messageNotifier.updateNotification(this)
}
}
deleteThreadAtCurrentPosition()
while (archivedConversations.moveToNext()) {
deleteThreadAtCurrentPosition()
}
}
2020-02-04 04:20:42 +01:00
// Double check that the long poller is up
2020-03-24 03:48:23 +01:00
(applicationContext as ApplicationContext).startPollingIfNeeded()
2019-12-17 14:27:59 +01:00
// Set content view
setContentView(R.layout.activity_home)
2020-01-07 02:00:30 +01:00
// Set custom toolbar
2020-01-06 02:07:55 +01:00
setSupportActionBar(toolbar)
2019-12-19 11:15:58 +01:00
// Set up Glide
glide = GlideApp.with(this)
2020-01-06 02:07:55 +01:00
// Set up toolbar buttons
profileButton.glide = glide
2020-07-15 06:26:20 +02:00
profileButton.publicKey = publicKey
2020-01-06 02:07:55 +01:00
profileButton.update()
profileButton.setOnClickListener { openSettings() }
2020-05-29 03:16:52 +02:00
pathStatusViewContainer.setOnClickListener { showPath() }
// Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
if (!hasViewedSeed && isMasterDevice) {
2020-05-25 07:46:53 +02:00
val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
seedReminderView.title = seedReminderViewTitle
2020-05-25 07:46:53 +02:00
seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1)
seedReminderView.setProgress(80, false)
seedReminderView.delegate = this
} else {
seedReminderView.visibility = View.GONE
}
2019-12-17 14:27:59 +01:00
// Set up recycler view
val cursor = DatabaseFactory.getThreadDatabase(this).conversationList
2019-12-19 11:15:58 +01:00
val homeAdapter = HomeAdapter(this, cursor)
homeAdapter.glide = glide
homeAdapter.conversationClickListener = this
recyclerView.adapter = homeAdapter
2019-12-17 14:27:59 +01:00
recyclerView.layoutManager = LinearLayoutManager(this)
2020-04-20 03:54:56 +02:00
// Set up empty state view
btnCreateNewPrivateChat.setOnClickListener { createNewPrivateChat() }
2019-12-19 11:49:23 +01:00
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return HomeLoader(this@HomeActivity)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
homeAdapter.changeCursor(cursor)
2020-04-20 03:54:56 +02:00
updateEmptyState()
2019-12-19 11:49:23 +01:00
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
homeAdapter.changeCursor(null)
}
})
2020-03-16 05:35:14 +01:00
// Set up gradient view
val gradientViewLayoutParams = gradientView.layoutParams as RelativeLayout.LayoutParams
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val height = displayMetrics.heightPixels
gradientViewLayoutParams.topMargin = (0.15 * height.toFloat()).toInt()
// Set up new conversation button set
newConversationButtonSet.delegate = this
2019-12-19 11:15:58 +01:00
// Set up typing observer
2019-12-19 11:49:23 +01:00
ApplicationContext.getInstance(this).typingStatusRepository.typingThreads.observe(this, Observer<Set<Long>> { threadIDs ->
val adapter = recyclerView.adapter as HomeAdapter
adapter.typingThreadIDs = threadIDs ?: setOf()
2019-12-19 11:15:58 +01:00
})
2020-05-14 05:52:20 +02:00
// Set up remaining components if needed
2020-05-26 00:55:29 +02:00
val application = ApplicationContext.getInstance(this)
2020-05-25 10:01:21 +02:00
val apiDB = DatabaseFactory.getLokiAPIDatabase(this)
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
val userDB = DatabaseFactory.getLokiUserDatabase(this)
val sskDatabase = DatabaseFactory.getSSKDatabase(this)
2020-05-14 05:52:20 +02:00
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
2020-07-15 06:26:20 +02:00
val sessionResetImpl = SessionResetImplementation(this)
2020-05-14 05:52:20 +02:00
if (userPublicKey != null) {
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey)
SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey)
2020-07-15 06:26:20 +02:00
application.publicChatManager.startPollersIfNeeded()
2019-12-17 14:27:59 +01:00
}
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application)
2020-05-25 10:01:21 +02:00
MultiDeviceProtocol.configureIfNeeded(apiDB)
2020-06-03 03:52:30 +02:00
IP2Country.configureIfNeeded(this)
// Preload device links to make message sending quicker
val publicKeys = ContactUtilities.getAllContacts(this).filter { contact ->
2020-07-15 06:26:20 +02:00
!contact.recipient.isGroupRecipient && !contact.isOurDevice && !contact.isSlave
}.map {
it.recipient.address.toPhoneString()
}.toSet()
2020-07-15 04:24:43 +02:00
FileServerAPI.shared.getDeviceLinks(publicKeys)
2020-07-16 04:49:37 +02:00
// Observe blocked contacts changed events
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
recyclerView.adapter!!.notifyDataSetChanged()
}
}
this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
2020-08-04 07:37:39 +02:00
// Clear all data if this is a secondary device
if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) {
TextSecurePreferences.setWasUnlinked(this, true)
2020-08-04 07:37:39 +02:00
ApplicationContext.getInstance(this).clearData()
}
2019-12-17 14:27:59 +01:00
}
override fun onResume() {
super.onResume()
2020-08-05 01:25:29 +02:00
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
profileButton.update()
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
if (hasViewedSeed || !isMasterDevice) {
seedReminderView.visibility = View.GONE
}
2020-07-30 08:53:34 +02:00
val hasSeenMultiDeviceRemovalSheet = TextSecurePreferences.getHasSeenMultiDeviceRemovalSheet(this)
if (!hasSeenMultiDeviceRemovalSheet) {
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
val deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getDeviceLinks(userPublicKey)
if (deviceLinks.isNotEmpty()) {
val bottomSheet = MultiDeviceRemovalBottomSheet()
bottomSheet.onOKTapped = {
bottomSheet.dismiss()
}
bottomSheet.onLinkTapped = {
bottomSheet.dismiss()
val url = "https://getsession.org/faq"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_CODE_CREATE_CLOSED_GROUP) {
createNewPrivateChat()
}
}
2020-07-16 04:49:37 +02:00
override fun onDestroy() {
val broadcastReceiver = this.broadcastReceiver
if (broadcastReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
}
super.onDestroy()
}
// endregion
2020-04-20 03:54:56 +02:00
// region Updating
private fun updateEmptyState() {
val threadCount = (recyclerView.adapter as HomeAdapter).itemCount
emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE
}
// endregion
// region Interaction
override fun handleSeedReminderViewContinueButtonTapped() {
val intent = Intent(this, SeedActivity::class.java)
show(intent)
}
2019-12-17 15:15:13 +01:00
override fun onConversationClick(view: ConversationView) {
val thread = view.thread ?: return
openConversation(thread)
}
override fun onLongConversationClick(view: ConversationView) {
val thread = view.thread ?: return
val bottomSheet = ConversationOptionsBottomSheet()
bottomSheet.recipient = thread.recipient
bottomSheet.onBlockOrUnblockTapped = {
bottomSheet.dismiss()
if (thread.recipient.isBlocked) {
unblockConversation(thread)
} else {
blockConversation(thread)
}
}
bottomSheet.onDeleteTapped = {
bottomSheet.dismiss()
deleteConversation(thread)
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
private fun blockConversation(thread: ThreadRecord) {
AlertDialog.Builder(this)
.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
Thread {
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, true)
ApplicationContext.getInstance(this).jobManager.add(MultiDeviceBlockedUpdateJob())
Util.runOnMain {
recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()
}
}.start()
}.show()
}
private fun unblockConversation(thread: ThreadRecord) {
AlertDialog.Builder(this)
.setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
Thread {
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, false)
ApplicationContext.getInstance(this).jobManager.add(MultiDeviceBlockedUpdateJob())
Util.runOnMain {
recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()
}
}.start()
}.show()
}
private fun deleteConversation(thread: ThreadRecord) {
val threadID = thread.threadId
val recipient = thread.recipient
val threadDB = DatabaseFactory.getThreadDatabase(this)
val deleteThread = object : Runnable {
override fun run() {
AsyncTask.execute {
val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID)
if (publicChat != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity)
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server)
}
threadDB.deleteConversation(threadID)
ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity)
}
}
}
val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message
val dialog = AlertDialog.Builder(this)
dialog.setMessage(dialogMessage)
dialog.setPositiveButton(R.string.yes) { _, _ ->
val isClosedGroup = recipient.address.isClosedGroup
// Send a leave group message if this is an active closed group
if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) {
2020-08-17 06:29:24 +02:00
val groupPublicKey = ClosedGroupsProtocol.doubleDecodeGroupID(recipient.address.toString()).toHexString()
2020-08-11 04:20:17 +02:00
val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey)
if (isSSKBasedClosedGroup) {
ClosedGroupsProtocol.leave(this, groupPublicKey)
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) {
Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
return@setPositiveButton
}
}
// Archive the conversation and then delete it after 10 seconds (the case where the
// app was closed before the conversation could be deleted is handled in onCreate)
threadDB.archiveConversation(threadID)
val delay = if (isClosedGroup) 10000L else 1000L
val handler = Handler()
handler.postDelayed(deleteThread, delay)
// Notify the user
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show()
}
dialog.setNegativeButton(R.string.no) { _, _ ->
// Do nothing
}
dialog.create().show()
2019-12-17 15:15:13 +01:00
}
private fun openConversation(thread: ThreadRecord) {
val intent = Intent(this, ConversationActivity::class.java)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, thread.recipient.address)
2019-12-17 15:15:13 +01:00
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, thread.threadId)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, thread.distributionType)
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis())
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, thread.lastSeen)
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1)
push(intent)
}
2020-01-06 02:07:55 +01:00
private fun openSettings() {
2020-01-06 04:26:52 +01:00
val intent = Intent(this, SettingsActivity::class.java)
show(intent)
2020-01-06 02:07:55 +01:00
}
2020-05-28 08:43:37 +02:00
private fun showPath() {
val intent = Intent(this, PathActivity::class.java)
show(intent)
}
override fun createNewPrivateChat() {
val intent = Intent(this, CreatePrivateChatActivity::class.java)
show(intent)
2019-12-17 15:15:13 +01:00
}
override fun createNewClosedGroup() {
2020-01-31 03:57:24 +01:00
val intent = Intent(this, CreateClosedGroupActivity::class.java)
show(intent, true)
2020-01-31 03:57:24 +01:00
}
override fun joinOpenGroup() {
val intent = Intent(this, JoinPublicChatActivity::class.java)
show(intent)
}
// endregion
2019-12-17 14:27:59 +01:00
}