package com.simplemobiletools.smsmessenger.activities import android.annotation.SuppressLint import android.app.Activity import android.app.role.RoleManager import android.content.Intent import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.provider.Telephony import android.text.TextUtils import androidx.coordinatorlayout.widget.CoordinatorLayout import com.simplemobiletools.commons.dialogs.PermissionRequiredDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.FAQItem import com.simplemobiletools.commons.models.Release import com.simplemobiletools.smsmessenger.BuildConfig import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter import com.simplemobiletools.smsmessenger.adapters.SearchResultsAdapter import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.models.Conversation import com.simplemobiletools.smsmessenger.models.Events import com.simplemobiletools.smsmessenger.models.Message import com.simplemobiletools.smsmessenger.models.SearchResult import kotlinx.android.synthetic.main.activity_main.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode class MainActivity : SimpleActivity() { private val MAKE_DEFAULT_APP_REQUEST = 1 private var storedTextColor = 0 private var storedFontSize = 0 private var lastSearchedText = "" private var bus: EventBus? = null private var wasProtectionHandled = false @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) appLaunched(BuildConfig.APPLICATION_ID) setupOptionsMenu() refreshMenuItems() updateMaterialActivityViews(main_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = true) if (savedInstanceState == null) { handleAppPasswordProtection { wasProtectionHandled = it if (it) { loadMessages() } else { finish() } } } if (checkAppSideloading()) { return } clearAllMessagesIfNeeded() } override fun onResume() { super.onResume() updateMenuColors() getOrCreateConversationsAdapter().apply { if (storedTextColor != getProperTextColor()) { updateTextColor(getProperTextColor()) } if (storedFontSize != config.fontSize) { updateFontSize() } updateDrafts() } updateTextColors(main_coordinator) search_holder.setBackgroundColor(getProperBackgroundColor()) val properPrimaryColor = getProperPrimaryColor() no_conversations_placeholder_2.setTextColor(properPrimaryColor) no_conversations_placeholder_2.underlineText() conversations_fastscroller.updateColors(properPrimaryColor) conversations_progress_bar.setIndicatorColor(properPrimaryColor) conversations_progress_bar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) checkShortcut() (conversations_fab?.layoutParams as? CoordinatorLayout.LayoutParams)?.bottomMargin = navigationBarHeight + resources.getDimension(R.dimen.activity_margin).toInt() } override fun onPause() { super.onPause() storeStateVariables() } override fun onDestroy() { super.onDestroy() bus?.unregister(this) } override fun onBackPressed() { if (main_menu.isSearchOpen) { main_menu.closeSearch() } else { super.onBackPressed() } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(WAS_PROTECTION_HANDLED, wasProtectionHandled) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) wasProtectionHandled = savedInstanceState.getBoolean(WAS_PROTECTION_HANDLED, false) if (!wasProtectionHandled) { handleAppPasswordProtection { wasProtectionHandled = it if (it) { loadMessages() } else { finish() } } } else { loadMessages() } } private fun setupOptionsMenu() { main_menu.getToolbar().inflateMenu(R.menu.menu_main) main_menu.toggleHideOnScroll(true) main_menu.setupMenu() main_menu.onSearchClosedListener = { fadeOutSearch() } main_menu.onSearchTextChangedListener = { text -> if (text.isNotEmpty()) { if (search_holder.alpha < 1f) { search_holder.fadeIn() } } else { fadeOutSearch() } searchTextChanged(text) } main_menu.getToolbar().setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.show_archived -> launchArchivedConversations() R.id.settings -> launchSettings() R.id.about -> launchAbout() else -> return@setOnMenuItemClickListener false } return@setOnMenuItemClickListener true } } private fun refreshMenuItems() { main_menu.getToolbar().menu.apply { findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(R.bool.hide_google_relations) } } override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { super.onActivityResult(requestCode, resultCode, resultData) if (requestCode == MAKE_DEFAULT_APP_REQUEST) { if (resultCode == Activity.RESULT_OK) { askPermissions() } else { finish() } } } private fun storeStateVariables() { storedTextColor = getProperTextColor() storedFontSize = config.fontSize } private fun updateMenuColors() { updateStatusbarColor(getProperBackgroundColor()) main_menu.updateColors() } private fun loadMessages() { if (isQPlus()) { val roleManager = getSystemService(RoleManager::class.java) if (roleManager!!.isRoleAvailable(RoleManager.ROLE_SMS)) { if (roleManager.isRoleHeld(RoleManager.ROLE_SMS)) { askPermissions() } else { val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_SMS) startActivityForResult(intent, MAKE_DEFAULT_APP_REQUEST) } } else { toast(R.string.unknown_error_occurred) finish() } } else { if (Telephony.Sms.getDefaultSmsPackage(this) == packageName) { askPermissions() } else { val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT) intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, packageName) startActivityForResult(intent, MAKE_DEFAULT_APP_REQUEST) } } } // while SEND_SMS and READ_SMS permissions are mandatory, READ_CONTACTS is optional. If we don't have it, we just won't be able to show the contact name in some cases private fun askPermissions() { handlePermission(PERMISSION_READ_SMS) { if (it) { handlePermission(PERMISSION_SEND_SMS) { if (it) { handlePermission(PERMISSION_READ_CONTACTS) { handleNotificationPermission { granted -> if (!granted) { PermissionRequiredDialog( activity = this, textId = R.string.allow_notifications_incoming_messages, positiveActionCallback = { openNotificationSettings() }) } } initMessenger() bus = EventBus.getDefault() try { bus!!.register(this) } catch (e: Exception) { } } } else { finish() } } } else { finish() } } } private fun initMessenger() { checkWhatsNewDialog() storeStateVariables() getCachedConversations() no_conversations_placeholder_2.setOnClickListener { launchNewConversation() } conversations_fab.setOnClickListener { launchNewConversation() } } private fun getCachedConversations() { ensureBackgroundThread { val conversations = try { conversationsDB.getNonArchived().toMutableList() as ArrayList } catch (e: Exception) { ArrayList() } val archived = try { conversationsDB.getAllArchived() } catch (e: Exception) { listOf() } updateUnreadCountBadge(conversations) runOnUiThread { setupConversations(conversations, cached = true) getNewConversations((conversations + archived).toMutableList() as ArrayList) } conversations.forEach { clearExpiredScheduledMessages(it.threadId) } } } private fun getNewConversations(cachedConversations: ArrayList) { val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) ensureBackgroundThread { val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) val conversations = getConversations(privateContacts = privateContacts) conversations.forEach { clonedConversation -> val threadIds = cachedConversations.map { it.threadId } if (!threadIds.contains(clonedConversation.threadId)) { conversationsDB.insertOrUpdate(clonedConversation) cachedConversations.add(clonedConversation) } } cachedConversations.forEach { cachedConversation -> val threadId = cachedConversation.threadId val isTemporaryThread = cachedConversation.isScheduled val isConversationDeleted = !conversations.map { it.threadId }.contains(threadId) if (isConversationDeleted && !isTemporaryThread) { conversationsDB.deleteThreadId(threadId) } val newConversation = conversations.find { it.phoneNumber == cachedConversation.phoneNumber } if (isTemporaryThread && newConversation != null) { // delete the original temporary thread and move any scheduled messages to the new thread conversationsDB.deleteThreadId(threadId) messagesDB.getScheduledThreadMessages(threadId) .forEach { message -> messagesDB.insertOrUpdate(message.copy(threadId = newConversation.threadId)) } insertOrUpdateConversation(newConversation, cachedConversation) } } cachedConversations.forEach { cachedConv -> val conv = conversations.find { it.threadId == cachedConv.threadId && !Conversation.areContentsTheSame(cachedConv, it) } if (conv != null) { val lastModified = maxOf(cachedConv.date, conv.date) val conversation = conv.copy(date = lastModified) insertOrUpdateConversation(conversation) } } val allConversations = conversationsDB.getNonArchived() as ArrayList runOnUiThread { setupConversations(allConversations) } if (config.appRunCount == 1) { conversations.map { it.threadId }.forEach { threadId -> val messages = getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false) messages.chunked(30).forEach { currentMessages -> messagesDB.insertMessages(*currentMessages.toTypedArray()) } } } } } private fun getOrCreateConversationsAdapter(): ConversationsAdapter { var currAdapter = conversations_list.adapter if (currAdapter == null) { hideKeyboard() currAdapter = ConversationsAdapter( activity = this, recyclerView = conversations_list, onRefresh = { notifyDatasetChanged() }, itemClick = { handleConversationClick(it) } ) conversations_list.adapter = currAdapter if (areSystemAnimationsEnabled) { conversations_list.scheduleLayoutAnimation() } } return currAdapter as ConversationsAdapter } private fun setupConversations(conversations: ArrayList, cached: Boolean = false) { val sortedConversations = conversations.sortedWith( compareByDescending { config.pinnedConversations.contains(it.threadId.toString()) } .thenByDescending { it.date } ).toMutableList() as ArrayList if (cached && config.appRunCount == 1) { // there are no cached conversations on the first run so we show the loading placeholder and progress until we are done loading from telephony showOrHideProgress(conversations.isEmpty()) } else { showOrHideProgress(false) showOrHidePlaceholder(conversations.isEmpty()) } try { getOrCreateConversationsAdapter().apply { updateConversations(sortedConversations) { if (!cached) { showOrHidePlaceholder(currentList.isEmpty()) } } } } catch (ignored: Exception) { } } private fun showOrHideProgress(show: Boolean) { if (show) { conversations_progress_bar.show() no_conversations_placeholder.beVisible() no_conversations_placeholder.text = getString(R.string.loading_messages) } else { conversations_progress_bar.hide() no_conversations_placeholder.beGone() } } private fun showOrHidePlaceholder(show: Boolean) { conversations_fastscroller.beGoneIf(show) no_conversations_placeholder.beVisibleIf(show) no_conversations_placeholder.text = getString(R.string.no_conversations_found) no_conversations_placeholder_2.beVisibleIf(show) } private fun fadeOutSearch() { search_holder.animate().alpha(0f).setDuration(SHORT_ANIMATION_DURATION).withEndAction { search_holder.beGone() searchTextChanged("", true) }.start() } @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, wasProtectionHandled) startActivity(this) } } private fun launchNewConversation() { hideKeyboard() Intent(this, NewConversationActivity::class.java).apply { startActivity(this) } } @SuppressLint("NewApi") private fun checkShortcut() { val appIconColor = config.appIconColor if (isNougatMR1Plus() && config.lastHandledShortcutColor != appIconColor) { val newConversation = getCreateNewContactShortcut(appIconColor) val manager = getSystemService(ShortcutManager::class.java) try { manager.dynamicShortcuts = listOf(newConversation) config.lastHandledShortcutColor = appIconColor } catch (ignored: Exception) { } } } @SuppressLint("NewApi") private fun getCreateNewContactShortcut(appIconColor: Int): ShortcutInfo { val newEvent = getString(R.string.new_conversation) val drawable = resources.getDrawable(R.drawable.shortcut_plus) (drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_plus_background).applyColorFilter(appIconColor) val bmp = drawable.convertToBitmap() val intent = Intent(this, NewConversationActivity::class.java) intent.action = Intent.ACTION_VIEW return ShortcutInfo.Builder(this, "new_conversation") .setShortLabel(newEvent) .setLongLabel(newEvent) .setIcon(Icon.createWithBitmap(bmp)) .setIntent(intent) .build() } private fun searchTextChanged(text: String, forceUpdate: Boolean = false) { if (!main_menu.isSearchOpen && !forceUpdate) { return } lastSearchedText = text search_placeholder_2.beGoneIf(text.length >= 2) if (text.length >= 2) { ensureBackgroundThread { val searchQuery = "%$text%" val messages = messagesDB.getMessagesWithText(searchQuery) val conversations = conversationsDB.getConversationsWithText(searchQuery) if (text == lastSearchedText) { showSearchResults(messages, conversations, text) } } } else { search_placeholder.beVisible() search_results_list.beGone() } } private fun showSearchResults(messages: List, conversations: List, searchedText: String) { val searchResults = ArrayList() conversations.forEach { conversation -> val date = conversation.date.formatDateOrTime(this, true, true) val searchResult = SearchResult(-1, conversation.title, conversation.phoneNumber, date, conversation.threadId, conversation.photoUri) searchResults.add(searchResult) } messages.sortedByDescending { it.id }.forEach { message -> var recipient = message.senderName if (recipient.isEmpty() && message.participants.isNotEmpty()) { val participantNames = message.participants.map { it.name } recipient = TextUtils.join(", ", participantNames) } val date = message.date.formatDateOrTime(this, true, true) val searchResult = SearchResult(message.id, recipient, message.body, date, message.threadId, message.senderPhotoUri) searchResults.add(searchResult) } runOnUiThread { search_results_list.beVisibleIf(searchResults.isNotEmpty()) search_placeholder.beVisibleIf(searchResults.isEmpty()) val currAdapter = search_results_list.adapter if (currAdapter == null) { SearchResultsAdapter(this, searchResults, search_results_list, searchedText) { hideKeyboard() Intent(this, ThreadActivity::class.java).apply { putExtra(THREAD_ID, (it as SearchResult).threadId) putExtra(THREAD_TITLE, it.title) putExtra(SEARCHED_MESSAGE_ID, it.messageId) startActivity(this) } }.apply { search_results_list.adapter = this } } else { (currAdapter as SearchResultsAdapter).updateItems(searchResults, searchedText) } } } private fun launchArchivedConversations() { hideKeyboard() startActivity(Intent(applicationContext, ArchivedConversationsActivity::class.java)) } private fun launchSettings() { hideKeyboard() startActivity(Intent(applicationContext, SettingsActivity::class.java)) } private fun launchAbout() { val licenses = LICENSE_EVENT_BUS or LICENSE_SMS_MMS or LICENSE_INDICATOR_FAST_SCROLL val faqItems = arrayListOf( FAQItem(R.string.faq_2_title, R.string.faq_2_text), FAQItem(R.string.faq_3_title, R.string.faq_3_text), FAQItem(R.string.faq_9_title_commons, R.string.faq_9_text_commons) ) if (!resources.getBoolean(R.bool.hide_google_relations)) { faqItems.add(FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons)) faqItems.add(FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons)) } startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) } @Subscribe(threadMode = ThreadMode.MAIN) fun refreshMessages(event: Events.RefreshMessages) { initMessenger() } private fun checkWhatsNewDialog() { arrayListOf().apply { add(Release(48, R.string.release_48)) add(Release(62, R.string.release_62)) checkWhatsNew(this, BuildConfig.VERSION_CODE) } } }