package com.simplemobiletools.smsmessenger.activities import android.annotation.SuppressLint import android.app.Activity import android.app.role.RoleManager import android.content.ActivityNotFoundException 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.net.Uri import android.os.Bundle import android.provider.Telephony import android.widget.Toast import com.simplemobiletools.commons.dialogs.FilePickerDialog 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.dialogs.ExportMessagesDialog import com.simplemobiletools.smsmessenger.dialogs.ImportMessagesDialog import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.EXPORT_MIME_TYPE import com.simplemobiletools.smsmessenger.helpers.MessagesExporter import com.simplemobiletools.smsmessenger.helpers.THREAD_ID import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE import com.simplemobiletools.smsmessenger.models.Conversation import com.simplemobiletools.smsmessenger.models.Events import kotlinx.android.synthetic.main.activity_main.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.io.FileOutputStream import java.io.OutputStream class MainActivity : SimpleActivity() { private val MAKE_DEFAULT_APP_REQUEST = 1 private val PICK_IMPORT_SOURCE_INTENT = 11 private val PICK_EXPORT_FILE_INTENT = 21 private var storedTextColor = 0 private var storedFontSize = 0 private var bus: EventBus? = null private val smsExporter by lazy { MessagesExporter(this) } @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) appLaunched(BuildConfig.APPLICATION_ID) setupOptionsMenu() refreshMenuItems() if (checkAppSideloading()) { return } 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) } } clearAllMessagesIfNeeded() } override fun onResume() { super.onResume() setupToolbar(main_toolbar) if (storedTextColor != getProperTextColor()) { (conversations_list.adapter as? ConversationsAdapter)?.updateTextColor(getProperTextColor()) } if (storedFontSize != config.fontSize) { (conversations_list.adapter as? ConversationsAdapter)?.updateFontSize() } (conversations_list.adapter as? ConversationsAdapter)?.updateDrafts() updateTextColors(main_coordinator) val properPrimaryColor = getProperPrimaryColor() no_conversations_placeholder_2.setTextColor(properPrimaryColor) no_conversations_placeholder_2.underlineText() conversations_fastscroller.updateColors(properPrimaryColor) checkShortcut() } override fun onPause() { super.onPause() storeStateVariables() } override fun onDestroy() { super.onDestroy() bus?.unregister(this) } private fun setupOptionsMenu() { main_toolbar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.search -> launchSearch() R.id.import_messages -> tryImportMessages() R.id.export_messages -> tryToExportMessages() R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.settings -> launchSettings() R.id.about -> launchAbout() else -> return@setOnMenuItemClickListener false } return@setOnMenuItemClickListener true } } private fun refreshMenuItems() { main_toolbar.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() } } else if (requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) { tryImportMessagesFromFile(resultData.data!!) } else if (requestCode == PICK_EXPORT_FILE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) { val outputStream = contentResolver.openOutputStream(resultData.data!!) exportMessagesTo(outputStream) } } private fun storeStateVariables() { storedTextColor = getProperTextColor() storedFontSize = config.fontSize } // 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) { toast(R.string.no_post_notifications_permissions) } } 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.getAll().toMutableList() as ArrayList } catch (e: Exception) { ArrayList() } updateUnreadCountBadge(conversations) runOnUiThread { setupConversations(conversations) getNewConversations(conversations) } 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)) } } } 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 usesCustomTitle = cachedConv.usesCustomTitle val title = if (usesCustomTitle) cachedConv.title else conv.title val conversation = conv.copy(date = lastModified, title = title, usesCustomTitle = usesCustomTitle) conversationsDB.insertOrUpdate(conversation) } } val allConversations = conversationsDB.getAll() 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 setupConversations(conversations: ArrayList) { val hasConversations = conversations.isNotEmpty() val sortedConversations = conversations.sortedWith( compareByDescending { config.pinnedConversations.contains(it.threadId.toString()) } .thenByDescending { it.date } ).toMutableList() as ArrayList conversations_fastscroller.beVisibleIf(hasConversations) no_conversations_placeholder.beGoneIf(hasConversations) no_conversations_placeholder_2.beGoneIf(hasConversations) if (!hasConversations && config.appRunCount == 1) { no_conversations_placeholder.text = getString(R.string.loading_messages) no_conversations_placeholder_2.beGone() } val currAdapter = conversations_list.adapter if (currAdapter == null) { hideKeyboard() ConversationsAdapter(this, conversations_list) { Intent(this, ThreadActivity::class.java).apply { val conversation = it as Conversation putExtra(THREAD_ID, conversation.threadId) putExtra(THREAD_TITLE, conversation.title) startActivity(this) } }.apply { conversations_list.adapter = this updateConversations(sortedConversations) } if (areSystemAnimationsEnabled) { conversations_list.scheduleLayoutAnimation() } } else { try { (currAdapter as ConversationsAdapter).updateConversations(sortedConversations) { if (currAdapter.currentList.isEmpty()) { conversations_fastscroller.beGone() no_conversations_placeholder.text = getString(R.string.no_conversations_found) no_conversations_placeholder.beVisible() no_conversations_placeholder_2.beVisible() } } } catch (ignored: Exception) { } } } 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 launchSearch() { hideKeyboard() startActivity(Intent(applicationContext, SearchActivity::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) } private fun tryToExportMessages() { if (isQPlus()) { ExportMessagesDialog(this, config.lastExportPath, true) { file -> Intent(Intent.ACTION_CREATE_DOCUMENT).apply { type = EXPORT_MIME_TYPE putExtra(Intent.EXTRA_TITLE, file.name) addCategory(Intent.CATEGORY_OPENABLE) try { startActivityForResult(this, PICK_EXPORT_FILE_INTENT) } catch (e: ActivityNotFoundException) { toast(R.string.system_service_disabled, Toast.LENGTH_LONG) } catch (e: Exception) { showErrorToast(e) } } } } else { handlePermission(PERMISSION_WRITE_STORAGE) { if (it) { ExportMessagesDialog(this, config.lastExportPath, false) { file -> getFileOutputStream(file.toFileDirItem(this), true) { outStream -> exportMessagesTo(outStream) } } } } } } private fun exportMessagesTo(outputStream: OutputStream?) { toast(R.string.exporting) ensureBackgroundThread { smsExporter.exportMessages(outputStream) { val toastId = when (it) { MessagesExporter.ExportResult.EXPORT_OK -> R.string.exporting_successful else -> R.string.exporting_failed } toast(toastId) } } } private fun tryImportMessages() { if (isQPlus()) { Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = EXPORT_MIME_TYPE try { startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT) } catch (e: ActivityNotFoundException) { toast(R.string.system_service_disabled, Toast.LENGTH_LONG) } catch (e: Exception) { showErrorToast(e) } } } else { handlePermission(PERMISSION_READ_STORAGE) { if (it) { importEvents() } } } } private fun importEvents() { FilePickerDialog(this) { showImportEventsDialog(it) } } private fun showImportEventsDialog(path: String) { ImportMessagesDialog(this, path) } private fun tryImportMessagesFromFile(uri: Uri) { when (uri.scheme) { "file" -> showImportEventsDialog(uri.path!!) "content" -> { val tempFile = getTempFile("messages", "backup.json") if (tempFile == null) { toast(R.string.unknown_error_occurred) return } try { val inputStream = contentResolver.openInputStream(uri) val out = FileOutputStream(tempFile) inputStream!!.copyTo(out) showImportEventsDialog(tempFile.absolutePath) } catch (e: Exception) { showErrorToast(e) } } else -> toast(R.string.invalid_file_format) } } @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) } } }