diff --git a/.gitignore b/.gitignore index aa724b7..03a5ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +/.idea diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 69e8615..5815a4a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6b0c3cf..ffa770c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,13 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'kotlin-android' + id 'kotlin-kapt' } android { namespace 'com.zeusinstitute.upiapp' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.zeusinstitute.upiapp" @@ -48,7 +50,21 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'com.journeyapps:zxing-android-embedded:4.3.0' + + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + def room_version = "2.5.2" + + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" // Use kapt instead of ksp + + // optional - Test helpers + testImplementation "androidx.room:room-testing:$room_version" + + // optional - Kotlin Extensions and Coroutines support for Room + implementation "androidx.room:room-ktx:$room_version" + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8de58be..c6e2db7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.UPIAPP" - tools:targetApi="18" > + tools:targetApi="18" + android:name=".UPIAPP"> + adapter.submitList(transactions) // Update adapter on main thread + Log.d("BillHistory", "Fetched ${transactions.size} transactions") + } + } + return view + } + private fun exportTransactionsToXML(transactions: List) { + try { + val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = docBuilder.newDocument() + val rootElement = doc.createElement("transactions") + doc.appendChild(rootElement) + + transactions.forEach { transaction -> + val transactionElement = doc.createElement("transaction") + rootElement.appendChild(transactionElement) + + val nameElement = doc.createElement("name") + nameElement.appendChild(doc.createTextNode(transaction.name)) + transactionElement.appendChild(nameElement) + + val typeElement = doc.createElement("type") + typeElement.appendChild(doc.createTextNode(transaction.type)) + transactionElement.appendChild(typeElement) + + val amountElement = doc.createElement("amount") + amountElement.appendChild(doc.createTextNode(transaction.amount.toString())) + transactionElement.appendChild(amountElement) + + val dateElement = doc.createElement("date") + dateElement.appendChild(doc.createTextNode(transaction.date)) + transactionElement.appendChild(dateElement) + } + + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + + val writer = StringWriter() + transformer.transform(DOMSource(doc), StreamResult(writer)) + val xmlString = writer.toString() + + // TODO: Save the xmlString to a file or share it as needed + println(xmlString) // Print XML to console for now + } catch (e: Exception) { + // Handle exceptions during XML creation + e.printStackTrace() + } + } + private fun refreshData() { + val sharedPref = requireActivity().getPreferences(Context.MODE_PRIVATE) + val smsEnabled = sharedPref.getBoolean("sms_enabled", false) + + warningTextView.visibility = if (smsEnabled) View.GONE else View.VISIBLE + if (!smsEnabled) { + warningTextView.text = "History Disabled, enable from Login." + return // Don't reload data if SMS is disabled + } + + lifecycleScope.launch { + val db = (requireContext().applicationContext as UPIAPP).database // Get centralized database + val transactionDao = db.transactionDao() + val transactions = withContext(Dispatchers.IO) { transactionDao.getAllTransactionsOrderedByDate().first() } + adapter.submitList(transactions) + Log.d("BillHistory", "Fetched ${transactions.size} transactions") + if (transactions.isEmpty()) { + warningTextView.text = "No Data Available" + warningTextView.visibility = View.VISIBLE + } else { + warningTextView.text = "" + warningTextView.visibility = View.GONE + } + } + } +} + diff --git a/app/src/main/java/com/zeusinstitute/upiapp/MIGRATION_1_2.kt b/app/src/main/java/com/zeusinstitute/upiapp/MIGRATION_1_2.kt new file mode 100644 index 0000000..1c6893d --- /dev/null +++ b/app/src/main/java/com/zeusinstitute/upiapp/MIGRATION_1_2.kt @@ -0,0 +1,10 @@ +package com.zeusinstitute.upiapp + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE PayTransaction ADD COLUMN name TEXT") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zeusinstitute/upiapp/MainActivity.kt b/app/src/main/java/com/zeusinstitute/upiapp/MainActivity.kt index cc659a4..5d3b357 100644 --- a/app/src/main/java/com/zeusinstitute/upiapp/MainActivity.kt +++ b/app/src/main/java/com/zeusinstitute/upiapp/MainActivity.kt @@ -77,6 +77,10 @@ class MainActivity : AppCompatActivity() { findNavController(R.id.nav_host_fragment_content_main).navigate(R.id.action_firstFragment_to_splitBillFragment) true } + R.id.billHistory -> { + findNavController(R.id.nav_host_fragment_content_main).navigate(R.id.action_firstFragment_to_billHistory) + true + } R.id.DynUPI -> { findNavController(R.id.nav_host_fragment_content_main).navigate(R.id.action_firstFragment_to_dynamicFragment) true diff --git a/app/src/main/java/com/zeusinstitute/upiapp/PayTransaction.kt b/app/src/main/java/com/zeusinstitute/upiapp/PayTransaction.kt new file mode 100644 index 0000000..b177848 --- /dev/null +++ b/app/src/main/java/com/zeusinstitute/upiapp/PayTransaction.kt @@ -0,0 +1,13 @@ +package com.zeusinstitute.upiapp + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class PayTransaction( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val name: String, + val amount: Double, + val type: String, // "Credit" or "Debit" + val date: String +) \ No newline at end of file diff --git a/app/src/main/java/com/zeusinstitute/upiapp/SMSService.kt b/app/src/main/java/com/zeusinstitute/upiapp/SMSService.kt index 03efb89..6f34b51 100644 --- a/app/src/main/java/com/zeusinstitute/upiapp/SMSService.kt +++ b/app/src/main/java/com/zeusinstitute/upiapp/SMSService.kt @@ -18,6 +18,9 @@ import androidx.core.app.NotificationCompat import java.util.* import java.util.concurrent.LinkedBlockingQueue import kotlinx.coroutines.* +import androidx.room.* +import java.text.SimpleDateFormat + class SMSService : Service(), TextToSpeech.OnInitListener { private var tts: TextToSpeech? = null @@ -28,6 +31,9 @@ class SMSService : Service(), TextToSpeech.OnInitListener { private val notificationChannelId = "sms_service_channel" private lateinit var notificationManager: NotificationManager + private lateinit var db: AppDatabase + lateinit var transactionDao: TransactionDao + companion object { const val STOP_SERVICE = "STOP_SERVICE" } @@ -35,24 +41,25 @@ class SMSService : Service(), TextToSpeech.OnInitListener { private val smsReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { - val messages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - Telephony.Sms.Intents.getMessagesFromIntent(intent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // For KitKat and above, use the bundled SMS API + val smsMessages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + smsMessages?.forEach { smsMessage -> + val messageBody = smsMessage.messageBody + processMessage(messageBody) + } } else { + // For older devices, parse SMS messages manually val bundle = intent.extras if (bundle != null) { val pdus = bundle["pdus"] as Array<*>? - pdus?.map { pdu -> - SmsMessage.createFromPdu(pdu as ByteArray) - }?.toTypedArray() - } else { - null + pdus?.forEach { pdu -> + val smsMessage = SmsMessage.createFromPdu(pdu as ByteArray) + val messageBody = smsMessage.messageBody + processMessage(messageBody) + } } } - - messages?.forEach { smsMessage -> - val messageBody = smsMessage.messageBody - processMessage(messageBody) - } } } } @@ -68,15 +75,32 @@ class SMSService : Service(), TextToSpeech.OnInitListener { override fun onCreate() { super.onCreate() tts = TextToSpeech(this, this) - registerReceiver(smsReceiver, IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) + + this.db = (applicationContext as UPIAPP).database + transactionDao = db.transactionDao() // Initialize transactionDao + + val smsIntentFilter = IntentFilter() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + smsIntentFilter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) + } else { + smsIntentFilter.addAction("android.provider.Telephony.SMS_RECEIVED") + } + registerReceiver(smsReceiver, smsIntentFilter) + startMessageProcessing() + val filter = IntentFilter(STOP_SERVICE) - registerReceiver(stopReceiver, filter) + + if (Build.VERSION.SDK_INT >= 33 ){ + registerReceiver(stopReceiver, filter, RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(stopReceiver, filter) + } notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Start the service in the foreground (for Android 8.0 and above) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel() // Create channel before starting foreground startForeground(notificationId, createNotification()) } else { @@ -97,36 +121,41 @@ class SMSService : Service(), TextToSpeech.OnInitListener { return } - if (message.contains("credited") && !message.contains("debited") && !message.contains("credited to")) { - val regex = "Rs\\.?\\s*(\\d+(\\.\\d{2})?)".toRegex() // Match Rs or Rs. - val matchResult = regex.find(message) - matchResult?.let { - val amount = it.groupValues[1] - val announcementMessage = "Received Rupees $amount" - Log.d("SMSService", "Queueing message: $announcementMessage") - messageQueue.offer(announcementMessage) - } ?: Log.d("SMSService", "No amount found in the message") - } else if (message.contains("debited")) { - val regex = "Rs\\.?\\s*(\\d+(\\.\\d{2})?)".toRegex() // Match Rs or Rs. - val matchResult = regex.find(message) - matchResult?.let { - val amount = it.groupValues[1] - val announcementMessage = "Sent Rupees $amount" - Log.d("SMSService", "Queueing message: $announcementMessage") - messageQueue.offer(announcementMessage) - } ?: Log.d("SMSService", "No amount found in the message") - } else if (message.contains("credited") && message.contains("credited to")) { - val regex = "Rs\\.?\\s*(\\d+(\\.\\d{2})?)".toRegex() // Match Rs or Rs. - val matchResult = regex.find(message) - matchResult?.let { - val amount = it.groupValues[1] - val announcementMessage = "Received Rupees $amount" + val regex = "Rs\\.?\\s*(\\d+(\\.\\d{2})?)".toRegex() + val matchResult = regex.find(message) + + var extractedName: String? = null + val nameRegex = "(?i)(?:from|From|FROM)\\s+(.*?)(?:\\.|thru|through)".toRegex() + val nameMatchResult = nameRegex.find(message) + extractedName = nameMatchResult?.groupValues?.getOrNull(1)?.trim() + + matchResult?.let { result -> + val amount = result.groupValues[1].toDoubleOrNull() + if (amount != null) { + val type = when { + message.contains("credited") && !message.contains("debited") -> "Credit" + message.contains("debited") -> "Debit" + else -> { + Log.d("SMSService", "Message does not match criteria for announcement") + return + } + } + + val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date()) + val transaction = PayTransaction(amount = amount, type = type, date = date, name = extractedName ?: "") // Use empty string if name is null + + scope.launch { + transactionDao.insert(transaction) + Log.d("SMSService", "Inserted transaction into database: $transaction") + } + + val announcementMessage = "${if (type == "Credit") "Received" else "Sent"} Rupees $amount" Log.d("SMSService", "Queueing message: $announcementMessage") messageQueue.offer(announcementMessage) - } ?: Log.d("SMSService", "No amount found in the message") - } else { - Log.d("SMSService", "Message does not match criteria for announcement") - } + } else { + Log.d("SMSService", "Invalid amount format in the message") + } + } ?: Log.d("SMSService", "No amount found in the message") } private fun startMessageProcessing() { @@ -164,10 +193,8 @@ class SMSService : Service(), TextToSpeech.OnInitListener { .setAutoCancel(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // For Android 8.0 and above, use the notification channel notificationManager.notify(notificationId, notificationBuilder.build()) } else { - // For older versions, show the notification directly @Suppress("DEPRECATION") notificationManager.notify(notificationId, notificationBuilder.build()) } @@ -186,6 +213,8 @@ class SMSService : Service(), TextToSpeech.OnInitListener { return notificationBuilder.build() } + + private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Check SDK version val channel = NotificationChannel( diff --git a/app/src/main/java/com/zeusinstitute/upiapp/TransactionAdapter.kt b/app/src/main/java/com/zeusinstitute/upiapp/TransactionAdapter.kt new file mode 100644 index 0000000..c24699d --- /dev/null +++ b/app/src/main/java/com/zeusinstitute/upiapp/TransactionAdapter.kt @@ -0,0 +1,49 @@ +package com.zeusinstitute.upiapp + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.zeusinstitute.upiapp.PayTransaction + +class TransactionAdapter : ListAdapter(TransactionDiffCallback()) { + + class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val transactionStatusTextView: TextView = itemView.findViewById(R.id.transactionStatusTextView) + val transactionIcon: ImageView = itemView.findViewById(R.id.transactionIcon) + val transactionNameTextView: TextView = itemView.findViewById(R.id.transactionNameTextView) + val transactionAmountTextView: TextView = itemView.findViewById(R.id.transactionAmountTextView) + val transactionDateTextView: TextView = itemView.findViewById(R.id.transactionDateTextView) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder { + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.transaction_item, parent, false) + return TransactionViewHolder(itemView) + } + + override fun onBindViewHolder(holder: TransactionViewHolder, position: Int) { + val transaction = getItem(position) + holder.transactionIcon.setImageResource( + if (transaction.type == "Debit") R.drawable.ic_debit + else R.drawable.ic_credit + ) + holder.transactionNameTextView.text = "${transaction.name}" + holder.transactionAmountTextView.text = "₹${transaction.amount}" + holder.transactionDateTextView.text = transaction.date + holder.transactionStatusTextView.text = if (transaction.type == "Debit") "Sent" else "Received" + } + + class TransactionDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: PayTransaction, newItem: PayTransaction): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: PayTransaction, newItem: PayTransaction): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zeusinstitute/upiapp/TransactionDao.kt b/app/src/main/java/com/zeusinstitute/upiapp/TransactionDao.kt new file mode 100644 index 0000000..1a2bbf7 --- /dev/null +++ b/app/src/main/java/com/zeusinstitute/upiapp/TransactionDao.kt @@ -0,0 +1,24 @@ +package com.zeusinstitute.upiapp + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransactionDao { + @Insert + fun insert(transaction: PayTransaction) + + @Query("SELECT * FROM PayTransaction ORDER BY date DESC") + fun getAll(): Flow> + + @Query("DELETE FROM PayTransaction") + fun deleteAll() + + @Query("SELECT * FROM PayTransaction") + fun getAllTransactions(): List + + @Query("SELECT * FROM PayTransaction ORDER BY date DESC") + fun getAllTransactionsOrderedByDate(): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/zeusinstitute/upiapp/UPIAPP.kt b/app/src/main/java/com/zeusinstitute/upiapp/UPIAPP.kt new file mode 100644 index 0000000..76538b3 --- /dev/null +++ b/app/src/main/java/com/zeusinstitute/upiapp/UPIAPP.kt @@ -0,0 +1,22 @@ +package com.zeusinstitute.upiapp + +// ... other imports ... +import android.app.Application +import androidx.room.Room +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class UPIAPP : Application() { + val database: AppDatabase by lazy { + val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE PayTransaction ADD COLUMN name TEXT NOT NULL") + } + } + + Room.databaseBuilder(this, AppDatabase::class.java, "transactions") + .addMigrations(MIGRATION_1_2) + .fallbackToDestructiveMigration() + .build() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_credit.xml b/app/src/main/res/drawable/ic_credit.xml new file mode 100644 index 0000000..d566a4c --- /dev/null +++ b/app/src/main/res/drawable/ic_credit.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_debit.xml b/app/src/main/res/drawable/ic_debit.xml new file mode 100644 index 0000000..9fa5e71 --- /dev/null +++ b/app/src/main/res/drawable/ic_debit.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bill_history.xml b/app/src/main/res/layout/fragment_bill_history.xml new file mode 100644 index 0000000..5587fcb --- /dev/null +++ b/app/src/main/res/layout/fragment_bill_history.xml @@ -0,0 +1,50 @@ + + + + + +