package org.thoughtcrime.securesms.loki.activities import android.annotation.SuppressLint import android.app.AlertDialog import android.arch.lifecycle.Observer import android.content.Intent import android.database.Cursor import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Paint import android.os.AsyncTask import android.os.Bundle import android.os.Handler import android.support.v4.app.LoaderManager import android.support.v4.content.Loader import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import android.support.v7.widget.helper.ItemTouchHelper import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.util.DisplayMetrics import android.view.View import android.widget.RelativeLayout import android.widget.Toast import kotlinx.android.synthetic.main.activity_home.* import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.loki.dialogs.PNModeBottomSheet import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.show import org.thoughtcrime.securesms.loki.views.ConversationView import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.notifications.MessageNotifier import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.protocol.friendrequests.FriendRequestProtocol import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol import org.whispersystems.signalservice.loki.protocol.syncmessages.SyncMessagesProtocol import kotlin.math.abs class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { private lateinit var glide: GlideRequests private val hexEncodedPublicKey: String get() { val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this) val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this) return masterHexEncodedPublicKey ?: userHexEncodedPublicKey } // region Lifecycle 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) MessageNotifier.updateNotification(this) } } deleteThreadAtCurrentPosition() while (archivedConversations.moveToNext()) { deleteThreadAtCurrentPosition() } } // Double check that the long poller is up (applicationContext as ApplicationContext).startPollingIfNeeded() // Set content view setContentView(R.layout.activity_home) // Set custom toolbar setSupportActionBar(toolbar) // Set up Glide glide = GlideApp.with(this) // Set up toolbar buttons profileButton.glide = glide profileButton.hexEncodedPublicKey = hexEncodedPublicKey profileButton.update() profileButton.setOnClickListener { openSettings() } // Set up seed reminder view val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null) val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (!hasViewedSeed && isMasterDevice) { val seedReminderViewTitle = SpannableString("You're almost finished! 80%") seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) seedReminderView.title = seedReminderViewTitle seedReminderView.subtitle = "Secure your account by saving your recovery phrase" seedReminderView.setProgress(80, false) seedReminderView.delegate = this } else { seedReminderView.visibility = View.GONE } // Set up recycler view val cursor = DatabaseFactory.getThreadDatabase(this).conversationList val homeAdapter = HomeAdapter(this, cursor) homeAdapter.glide = glide homeAdapter.conversationClickListener = this recyclerView.adapter = homeAdapter recyclerView.layoutManager = LinearLayoutManager(this) ItemTouchHelper(SwipeCallback(this)).attachToRecyclerView(recyclerView) // Set up empty state view createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } // 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 { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { return HomeLoader(this@HomeActivity) } override fun onLoadFinished(loader: Loader, cursor: Cursor?) { homeAdapter.changeCursor(cursor) updateEmptyState() } override fun onLoaderReset(cursor: Loader) { homeAdapter.changeCursor(null) } }) // 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 // Set up typing observer ApplicationContext.getInstance(this).typingStatusRepository.typingThreads.observe(this, Observer> { threadIDs -> val adapter = recyclerView.adapter as HomeAdapter adapter.typingThreadIDs = threadIDs ?: setOf() }) // Set up remaining components if needed val userPublicKey = TextSecurePreferences.getLocalNumber(this) if (userPublicKey != null) { val application = ApplicationContext.getInstance(this) val apiDB = DatabaseFactory.getLokiAPIDatabase(this) val threadDB = DatabaseFactory.getLokiThreadDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this) val sessionResetImpl = LokiSessionResetImplementation(this) FriendRequestProtocol.configureIfNeeded(apiDB, userPublicKey) MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB) SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey) org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol.configureIfNeeded(apiDB) SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application) SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey) application.lokiPublicChatManager.startPollersIfNeeded() } } override fun onResume() { super.onResume() val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null) val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (hasViewedSeed || !isMasterDevice) { seedReminderView.visibility = View.GONE } if (!TextSecurePreferences.hasSeenPNModeSheet(this)) { val bottomSheet = PNModeBottomSheet() bottomSheet.onConfirmTapped = { isUsingFCM -> TextSecurePreferences.setHasSeenPNModeSheet(this, true) TextSecurePreferences.setIsUsingFCM(this, isUsingFCM) ApplicationContext.getInstance(this).registerForFCMIfNeeded(true) bottomSheet.dismiss() } bottomSheet.onSkipTapped = { TextSecurePreferences.setHasSeenPNModeSheet(this, true) bottomSheet.dismiss() } bottomSheet.show(supportFragmentManager, bottomSheet.tag) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == CreateClosedGroupActivity.createNewPrivateChatResultCode) { createNewPrivateChat() } } // endregion // 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) } override fun onConversationClick(view: ConversationView) { val thread = view.thread ?: return openConversation(thread) } override fun onLongConversationClick(view: ConversationView) { // Do nothing } private fun openConversation(thread: ThreadRecord) { val intent = Intent(this, ConversationActivity::class.java) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, thread.recipient.address) 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) } private fun openSettings() { val intent = Intent(this, SettingsActivity::class.java) show(intent) } override fun createNewPrivateChat() { val intent = Intent(this, CreatePrivateChatActivity::class.java) show(intent) } override fun createNewClosedGroup() { val intent = Intent(this, CreateClosedGroupActivity::class.java) show(intent, true) } override fun joinOpenGroup() { val intent = Intent(this, JoinPublicChatActivity::class.java) show(intent) } private class SwipeCallback(val activity: HomeActivity) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false } @SuppressLint("StaticFieldLeak") override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { viewHolder as HomeAdapter.ViewHolder val threadID = viewHolder.view.thread!!.threadId val recipient = viewHolder.view.thread!!.recipient val threadDatabase = DatabaseFactory.getThreadDatabase(activity) val deleteThread = object : Runnable { override fun run() { AsyncTask.execute { val publicChat = DatabaseFactory.getLokiThreadDatabase(activity).getPublicChat(threadID) if (publicChat != null) { val apiDatabase = DatabaseFactory.getLokiAPIDatabase(activity) apiDatabase.removeLastMessageServerID(publicChat.channel, publicChat.server) apiDatabase.removeLastDeletionServerID(publicChat.channel, publicChat.server) ApplicationContext.getInstance(activity).lokiPublicChatAPI!!.leave(publicChat.channel, publicChat.server) } threadDatabase.deleteConversation(threadID) MessageNotifier.updateNotification(activity) } } } 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(activity) 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(activity).isActive(recipient.address.toGroupString())) { if (!ClosedGroupsProtocol.leaveGroup(activity, recipient)) { Toast.makeText(activity, "Couldn't leave group", Toast.LENGTH_LONG).show() clearView(activity.recyclerView, viewHolder) 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) threadDatabase.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(activity, toastMessage, Toast.LENGTH_LONG).show() } dialog.setNegativeButton(R.string.no) { _, _ -> clearView(activity.recyclerView, viewHolder) } dialog.create().show() } override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dx: Float, dy: Float, actionState: Int, isCurrentlyActive: Boolean) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && dx < 0) { val itemView = viewHolder.itemView animate(viewHolder, dx) val backgroundPaint = Paint() backgroundPaint.color = activity.resources.getColorWithID(R.color.destructive, activity.theme) c.drawRect(itemView.right.toFloat() - abs(dx), itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat(), backgroundPaint) val icon = BitmapFactory.decodeResource(activity.resources, R.drawable.ic_trash_filled_32) val iconPaint = Paint() val left = itemView.right.toFloat() - abs(dx) + activity.resources.getDimension(R.dimen.medium_spacing) val top = itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - icon.height) / 2 c.drawBitmap(icon, left, top, iconPaint) } else { super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive) } } private fun animate(viewHolder: RecyclerView.ViewHolder, dx: Float) { val alpha = 1.0f - abs(dx) / viewHolder.itemView.width.toFloat() viewHolder.itemView.alpha = alpha viewHolder.itemView.translationX = dx } override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) viewHolder.itemView.alpha = 1.0f viewHolder.itemView.translationX = 0.0f } } // endregion }