diff --git a/CODEOWNERS b/CODEOWNERS
index be4069a04..5f6843223 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1 @@
-* @naveensingh
\ No newline at end of file
+* @adarshkannada
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ab24b77e8..4c1352527 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -29,8 +29,8 @@ base {
}
android {
- compileSdk = project.libs.versions.app.build.compileSDKVersion.get().toInt()
+ compileSdkVersion(project.libs.versions.app.build.compileSDKVersion.get().toInt())
defaultConfig {
applicationId = project.property("APP_ID").toString()
minSdk = project.libs.versions.app.build.minimumSDK.get().toInt()
@@ -131,6 +131,9 @@ android {
enableSplit = false
}
}
+ kotlin {
+ jvmToolchain(21)
+ }
}
detekt {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2fa856ddb..33dc2b583 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,8 @@
+
+
diff --git a/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt
index fdf98661d..2beb163f0 100644
--- a/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt
+++ b/app/src/main/kotlin/org/fossify/messages/activities/MainActivity.kt
@@ -11,6 +11,9 @@ import android.os.Bundle
import android.provider.Telephony
import android.text.TextUtils
import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.PopupMenu
+import androidx.appcompat.view.ContextThemeWrapper
+import org.fossify.commons.extensions.getPopupMenuTheme
import org.fossify.commons.dialogs.PermissionRequiredDialog
import org.fossify.commons.extensions.adjustAlpha
import org.fossify.commons.extensions.appLaunched
@@ -79,6 +82,13 @@ class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1
+ private enum class Tab {
+ PERSONAL, FINANCIAL, OTHERS
+ }
+
+ private var currentTab = Tab.PERSONAL
+ private var allLoadedConversations = ArrayList()
+
private var storedTextColor = 0
private var storedFontSize = 0
private var lastSearchedText = ""
@@ -92,9 +102,14 @@ class MainActivity : SimpleActivity() {
setContentView(binding.root)
appLaunched(BuildConfig.APPLICATION_ID)
setupOptionsMenu()
- refreshMenuItems()
- setupEdgeToEdge(padBottomImeAndSystem = listOf(binding.conversationsList))
+ if (savedInstanceState != null) {
+ val tabOrdinal = savedInstanceState.getInt("current_tab", Tab.PERSONAL.ordinal)
+ currentTab = Tab.values()[tabOrdinal]
+ }
+
+ setupEdgeToEdge(padBottomImeAndSystem = listOf(binding.conversationsList, binding.bottomNavigationBar))
+ setupBottomNavigation()
checkAndDeleteOldRecycleBinMessages()
clearAllMessagesIfNeeded {
@@ -106,10 +121,15 @@ class MainActivity : SimpleActivity() {
}
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt("current_tab", currentTab.ordinal)
+ }
+
override fun onResume() {
super.onResume()
updateMenuColors()
- refreshMenuItems()
+ updateBottomBar()
getOrCreateConversationsAdapter().apply {
if (storedTextColor != getProperTextColor()) {
@@ -156,7 +176,6 @@ class MainActivity : SimpleActivity() {
}
private fun setupOptionsMenu() {
- binding.mainMenu.requireToolbar().inflateMenu(R.menu.menu_main)
binding.mainMenu.toggleHideOnScroll(true)
binding.mainMenu.setupMenu()
@@ -174,24 +193,17 @@ class MainActivity : SimpleActivity() {
}
searchTextChanged(text)
}
-
- binding.mainMenu.requireToolbar().setOnMenuItemClickListener { menuItem ->
- when (menuItem.itemId) {
- R.id.show_recycle_bin -> launchRecycleBin()
- R.id.show_archived -> launchArchivedConversations()
- R.id.settings -> launchSettings()
- R.id.about -> launchAbout()
- else -> return@setOnMenuItemClickListener false
- }
- return@setOnMenuItemClickListener true
- }
}
- private fun refreshMenuItems() {
- binding.mainMenu.requireToolbar().menu.apply {
- findItem(R.id.show_recycle_bin).isVisible = config.useRecycleBin
- findItem(R.id.show_archived).isVisible = config.isArchiveAvailable
+ private fun handleMenuItemClick(itemId: Int): Boolean {
+ when (itemId) {
+ R.id.show_recycle_bin -> launchRecycleBin()
+ R.id.show_archived -> launchArchivedConversations()
+ R.id.settings -> launchSettings()
+ R.id.about -> launchAbout()
+ else -> return false
}
+ return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
@@ -403,6 +415,8 @@ class MainActivity : SimpleActivity() {
conversations: ArrayList,
cached: Boolean = false,
) {
+ allLoadedConversations = conversations
+
val sortedConversations = conversations
.sortedWith(
compareByDescending {
@@ -410,18 +424,24 @@ class MainActivity : SimpleActivity() {
}.thenByDescending { it.date }
).toMutableList() as ArrayList
+ val filteredConversations = when (currentTab) {
+ Tab.PERSONAL -> sortedConversations.filter { isPersonal(it) }
+ Tab.FINANCIAL -> sortedConversations.filter { isFinancial(it) }
+ Tab.OTHERS -> sortedConversations.filter { !isPersonal(it) && !isFinancial(it) }
+ }.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())
+ showOrHidePlaceholder(filteredConversations.isEmpty())
}
try {
getOrCreateConversationsAdapter().apply {
- updateConversations(sortedConversations) {
+ updateConversations(filteredConversations) {
if (!cached) {
showOrHidePlaceholder(currentList.isEmpty())
}
@@ -682,4 +702,125 @@ class MainActivity : SimpleActivity() {
checkWhatsNew(this, BuildConfig.VERSION_CODE)
}
}
+
+ private fun isFinancial(conversation: Conversation): Boolean {
+ val text = (conversation.snippet + " " + conversation.title).lowercase()
+ val financialKeywords = listOf(
+ "bank", "account", "txn", "transaction", "debit", "credit", "otp",
+ "spent", "withdraw", "rs.", "inr", "paytm", "gpay", "card", "payment"
+ )
+ return financialKeywords.any { text.contains(it) }
+ }
+
+ private fun isPersonal(conversation: Conversation): Boolean {
+ if (isFinancial(conversation)) return false
+
+ val number = conversation.phoneNumber.trim()
+ if (number.isEmpty()) return false
+
+ if (conversation.title != conversation.phoneNumber) {
+ return true
+ }
+
+ val digitsAndSymbols = number.filter { it.isDigit() || it == '+' || it == '-' || it == '(' || it == ')' || it.isWhitespace() }
+ if (digitsAndSymbols.length != number.length) {
+ return false
+ }
+
+ val digitCount = number.filter { it.isDigit() }.length
+ if (digitCount < 7) {
+ return false
+ }
+
+ return true
+ }
+
+ private fun setupBottomNavigation() {
+ binding.tabPersonalBtn.setOnClickListener {
+ if (currentTab != Tab.PERSONAL) {
+ currentTab = Tab.PERSONAL
+ updateBottomBar()
+ setupConversations(allLoadedConversations)
+ }
+ }
+
+ binding.tabFinancialBtn.setOnClickListener {
+ if (currentTab != Tab.FINANCIAL) {
+ currentTab = Tab.FINANCIAL
+ updateBottomBar()
+ setupConversations(allLoadedConversations)
+ }
+ }
+
+ binding.tabOthersBtn.setOnClickListener {
+ if (currentTab != Tab.OTHERS) {
+ currentTab = Tab.OTHERS
+ updateBottomBar()
+ setupConversations(allLoadedConversations)
+ }
+ }
+
+ binding.tabMenuBtn.setOnClickListener {
+ showPopupMenu()
+ }
+ }
+
+ private fun showPopupMenu() {
+ val theme = getPopupMenuTheme()
+ val contextTheme = ContextThemeWrapper(this, theme)
+ val popup = PopupMenu(contextTheme, binding.tabMenuBtn)
+ popup.menuInflater.inflate(R.menu.menu_main, popup.menu)
+ popup.menu.apply {
+ findItem(R.id.show_recycle_bin).isVisible = config.useRecycleBin
+ findItem(R.id.show_archived).isVisible = config.isArchiveAvailable
+ }
+ popup.setOnMenuItemClickListener { menuItem ->
+ handleMenuItemClick(menuItem.itemId)
+ }
+ popup.show()
+ }
+
+ private fun updateBottomBar() {
+ val primaryColor = getProperPrimaryColor()
+ val textColor = getProperTextColor()
+ val inactiveColor = textColor.adjustAlpha(0.6f)
+
+ binding.bottomNavigationBar.setBackgroundColor(getProperBackgroundColor())
+
+ if (currentTab == Tab.PERSONAL) {
+ binding.tabPersonalIcon.applyColorFilter(primaryColor)
+ binding.tabPersonalLabel.setTextColor(primaryColor)
+ binding.tabPersonalLabel.paint.isFakeBoldText = true
+ } else {
+ binding.tabPersonalIcon.applyColorFilter(inactiveColor)
+ binding.tabPersonalLabel.setTextColor(inactiveColor)
+ binding.tabPersonalLabel.paint.isFakeBoldText = false
+ }
+
+ if (currentTab == Tab.FINANCIAL) {
+ binding.tabFinancialIcon.applyColorFilter(primaryColor)
+ binding.tabFinancialLabel.setTextColor(primaryColor)
+ binding.tabFinancialLabel.paint.isFakeBoldText = true
+ } else {
+ binding.tabFinancialIcon.applyColorFilter(inactiveColor)
+ binding.tabFinancialLabel.setTextColor(inactiveColor)
+ binding.tabFinancialLabel.paint.isFakeBoldText = false
+ }
+
+ if (currentTab == Tab.OTHERS) {
+ binding.tabOthersIcon.applyColorFilter(primaryColor)
+ binding.tabOthersLabel.setTextColor(primaryColor)
+ binding.tabOthersLabel.paint.isFakeBoldText = true
+ } else {
+ binding.tabOthersIcon.applyColorFilter(inactiveColor)
+ binding.tabOthersLabel.setTextColor(inactiveColor)
+ binding.tabOthersLabel.paint.isFakeBoldText = false
+ }
+
+ // Menu item is not a tab, so it always uses the inactive color
+ binding.tabMenuIcon.applyColorFilter(inactiveColor)
+ binding.tabMenuLabel.setTextColor(inactiveColor)
+ binding.tabMenuLabel.paint.isFakeBoldText = false
+ }
}
+
diff --git a/app/src/main/res/drawable/ic_tab_financial.xml b/app/src/main/res/drawable/ic_tab_financial.xml
new file mode 100644
index 000000000..4c1a08110
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tab_financial.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_tab_menu.xml b/app/src/main/res/drawable/ic_tab_menu.xml
new file mode 100644
index 000000000..df3371f9f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tab_menu.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_tab_others.xml b/app/src/main/res/drawable/ic_tab_others.xml
new file mode 100644
index 000000000..82cb51a25
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tab_others.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_tab_personal.xml b/app/src/main/res/drawable/ic_tab_personal.xml
new file mode 100644
index 000000000..d8b5cb48b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tab_personal.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f70ffdd20..3ff5ba443 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -79,6 +79,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
+ android:paddingBottom="88dp"
android:layoutAnimation="@anim/layout_animation"
android:overScrollMode="ifContentScrolls"
android:scrollbars="none"
@@ -130,18 +131,140 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
+ android:paddingBottom="88dp"
android:scrollbars="vertical"
app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 07213302a..42ea4a0fc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -140,7 +140,11 @@
Why do my group message replies appear as individual texts to recipients?
You need to enable \"Send group messages as MMS\" in the app settings. This will ensure that group messages are sent as proper MMS, allowing all participants to see each other\'s replies.
+ Personal
+ Financial
+ Others
+ Menu
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 11fa6e4ce..33c8aedf8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,13 +21,13 @@ commons = "6.1.6"
mmslib = "1.0.0"
indicator-fast-scroll = "4524cd0b61"
#Gradle
-gradlePlugins-agp = "9.2.0"
+gradlePlugins-agp = "9.0.1"
#build
app-build-compileSDKVersion = "36"
app-build-targetSDK = "36"
app-build-minimumSDK = "26"
-app-build-javaVersion = "VERSION_17"
-app-build-kotlinJVMTarget = "17"
+app-build-javaVersion = "VERSION_21"
+app-build-kotlinJVMTarget = "21"
#Helpers
ez-vcard = "0.12.2"
[libraries]
diff --git a/how-to-build.md b/how-to-build.md
new file mode 100644
index 000000000..47269e643
--- /dev/null
+++ b/how-to-build.md
@@ -0,0 +1,9 @@
+## run this in the terminal
+#### Example path - find your actual 'jbr' folder path
+to build the app, set JDK path available through android studio
+my case it was below On a linux machine.
+
+`export JAVA_HOME="/opt/android-studio/jbr"`
+
+command to trigger the build
+`./gradlew :app:assembleFossDebug`