@@ -6,7 +6,10 @@ import android.content.Context
6
6
import android.content.Intent
7
7
import android.content.IntentFilter
8
8
import android.content.SharedPreferences
9
+ import android.content.pm.PackageManager
10
+ import android.os.Build
9
11
import android.os.Bundle
12
+ import android.os.Environment
10
13
import android.provider.Settings
11
14
import android.text.TextUtils
12
15
import android.widget.Button
@@ -15,9 +18,16 @@ import android.widget.TextView
15
18
import android.widget.Toast
16
19
import android.widget.ProgressBar
17
20
import androidx.appcompat.app.AppCompatActivity
21
+ import androidx.core.app.ActivityCompat
22
+ import androidx.core.content.ContextCompat
23
+ import androidx.core.content.FileProvider
18
24
import androidx.recyclerview.widget.LinearLayoutManager
19
25
import androidx.recyclerview.widget.RecyclerView
20
26
import androidx.lifecycle.lifecycleScope
27
+ import java.io.File
28
+ import java.io.FileWriter
29
+ import java.text.SimpleDateFormat
30
+ import java.util.Locale
21
31
import kotlinx.coroutines.Dispatchers
22
32
import kotlinx.coroutines.launch
23
33
import kotlinx.coroutines.withContext
@@ -51,6 +61,7 @@ class MainActivity : AppCompatActivity() {
51
61
private lateinit var btnRequestPermission: Button
52
62
private lateinit var btnAppFilters: Button
53
63
private lateinit var btnViewLocalStorage: Button
64
+ private lateinit var btnExportCsv: Button
54
65
private lateinit var btnClearDatabase: Button
55
66
private lateinit var btnSettings: Button
56
67
private lateinit var recyclerView: RecyclerView
@@ -121,6 +132,7 @@ class MainActivity : AppCompatActivity() {
121
132
btnRequestPermission = findViewById(R .id.btnRequestPermission)
122
133
btnAppFilters = findViewById(R .id.btnAppFilters)
123
134
btnViewLocalStorage = findViewById(R .id.btnViewLocalStorage)
135
+ btnExportCsv = findViewById(R .id.btnExportCsv)
124
136
btnClearDatabase = findViewById(R .id.btnClearDatabase)
125
137
btnSettings = findViewById(R .id.btnSettings)
126
138
recyclerView = findViewById(R .id.recyclerViewNotifications)
@@ -140,6 +152,7 @@ class MainActivity : AppCompatActivity() {
140
152
btnSyncNow.setOnClickListener { syncData() }
141
153
btnAppFilters.setOnClickListener { openAppFilterSettings() }
142
154
btnViewLocalStorage.setOnClickListener { viewLocalStorage() }
155
+ btnExportCsv.setOnClickListener { exportCsvDirectly() }
143
156
btnClearDatabase.setOnClickListener { clearDatabase() }
144
157
btnSettings.setOnClickListener { openSettings() }
145
158
@@ -666,5 +679,141 @@ class MainActivity : AppCompatActivity() {
666
679
btnClearDatabase.isEnabled = enabled
667
680
btnAppFilters.isEnabled = enabled
668
681
btnViewLocalStorage.isEnabled = enabled
682
+ btnExportCsv.isEnabled = enabled
683
+ }
684
+
685
+ /* *
686
+ * Direct CSV export from main screen - single-click solution
687
+ */
688
+ private fun exportCsvDirectly () {
689
+ // Check permissions for older Android versions
690
+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .Q && Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
691
+ if (ContextCompat .checkSelfPermission(this , android.Manifest .permission.WRITE_EXTERNAL_STORAGE )
692
+ != PackageManager .PERMISSION_GRANTED ) {
693
+ ActivityCompat .requestPermissions(
694
+ this ,
695
+ arrayOf(android.Manifest .permission.WRITE_EXTERNAL_STORAGE ),
696
+ 1001
697
+ )
698
+ return
699
+ }
700
+ }
701
+
702
+ showLoading(true )
703
+ setButtonsEnabled(false )
704
+ btnExportCsv.text = " 📁 Exporting..."
705
+
706
+ lifecycleScope.launch {
707
+ try {
708
+ val (fileName, fileUri) = withContext(kotlinx.coroutines.Dispatchers .IO ) {
709
+ val notifications = database.notificationDao().getAllNotifications()
710
+
711
+ if (notifications.isEmpty()) {
712
+ throw Exception (" No notifications to export" )
713
+ }
714
+
715
+ val timestamp = SimpleDateFormat (" yyyyMMdd_HHmmss" , Locale .getDefault()).format(java.util.Date ())
716
+ val fileName = " noto_data_export_$timestamp .csv"
717
+
718
+ // Use app-specific external storage (no permission needed on Android 10+)
719
+ val exportDir = File (getExternalFilesDir(Environment .DIRECTORY_DOCUMENTS ), " exports" )
720
+ if (! exportDir.exists()) {
721
+ exportDir.mkdirs()
722
+ }
723
+
724
+ val file = File (exportDir, fileName)
725
+
726
+ // Write CSV file
727
+ try {
728
+ FileWriter (file).use { writer ->
729
+ // Write CSV header with all fields including new persistent notification fields
730
+ writer.append(" ID,Device ID,Dataset Name,Package Name,App Label,Title,Text,Sender,Timestamp,Category,Channel ID,Extras,Is Synced,Created At,Is Persistent,Notification Flags,Notification Key,Progress Current,Progress Max,Is Duplicate Detected\n " )
731
+
732
+ // Write notification data with new persistent fields
733
+ notifications.forEach { notification ->
734
+ writer.append(" ${notification.id} ," )
735
+ writer.append(" \" ${notification.deviceId} \" ," )
736
+ writer.append(" \" ${notification.datasetName} \" ," )
737
+ writer.append(" \" ${notification.packageName} \" ," )
738
+ writer.append(" \" ${notification.appLabel ? : " " } \" ," )
739
+ writer.append(" \" ${notification.title?.replace(" \" " , " \"\" " ) ? : " " } \" ," )
740
+ writer.append(" \" ${notification.text?.replace(" \" " , " \"\" " ) ? : " " } \" ," )
741
+ writer.append(" \" ${notification.sender?.replace(" \" " , " \"\" " ) ? : " " } \" ," )
742
+ writer.append(" \" ${notification.timestamp} \" ," )
743
+ writer.append(" \" ${notification.category ? : " " } \" ," )
744
+ writer.append(" \" ${notification.channelId ? : " " } \" ," )
745
+ writer.append(" \" ${notification.extras ? : " " } \" ," )
746
+ writer.append(" ${notification.synced} ," )
747
+ writer.append(" \" ${notification.createdAt} \" ," )
748
+ // New persistent notification fields
749
+ writer.append(" ${notification.isPersistent} ," )
750
+ writer.append(" ${notification.notificationFlags} ," )
751
+ writer.append(" \" ${notification.notificationKey ? : " " } \" ," )
752
+ writer.append(" ${notification.progressCurrent} ," )
753
+ writer.append(" ${notification.progressMax} ," )
754
+ writer.append(" ${notification.isDuplicateDetected} \n " )
755
+ }
756
+ }
757
+ } catch (e: Exception ) {
758
+ throw Exception (" Failed to write CSV file: ${e.message} " , e)
759
+ }
760
+
761
+ val fileUri = FileProvider .getUriForFile(
762
+ this @MainActivity,
763
+ " ${applicationContext.packageName} .fileprovider" ,
764
+ file
765
+ )
766
+
767
+ Pair (fileName, fileUri)
768
+ }
769
+
770
+ showLoading(false )
771
+ setButtonsEnabled(true )
772
+ btnExportCsv.text = " 📁 Export CSV"
773
+
774
+ // Show success and offer to share
775
+ androidx.appcompat.app.AlertDialog .Builder (this @MainActivity)
776
+ .setTitle(" Export Successful" )
777
+ .setMessage(" Data exported to: $fileName \n\n Would you like to share the file?" )
778
+ .setPositiveButton(" Share" ) { _, _ ->
779
+ shareExportedFile(fileUri, fileName)
780
+ }
781
+ .setNegativeButton(" OK" , null )
782
+ .show()
783
+
784
+ } catch (e: Exception ) {
785
+ android.util.Log .e(" MainActivity" , " Error exporting data" , e)
786
+
787
+ showLoading(false )
788
+ setButtonsEnabled(true )
789
+ btnExportCsv.text = " 📁 Export CSV"
790
+
791
+ Toast .makeText(this @MainActivity, " Export failed: ${e.message} " , Toast .LENGTH_LONG ).show()
792
+ }
793
+ }
794
+ }
795
+
796
+ private fun shareExportedFile (fileUri : android.net.Uri , fileName : String ) {
797
+ try {
798
+ val shareIntent = Intent ().apply {
799
+ action = Intent .ACTION_SEND
800
+ type = " text/csv"
801
+ putExtra(Intent .EXTRA_STREAM , fileUri)
802
+ putExtra(Intent .EXTRA_SUBJECT , " Noto Data Export - $fileName " )
803
+ putExtra(Intent .EXTRA_TEXT , " Exported notification data from Noto app" )
804
+ addFlags(Intent .FLAG_GRANT_READ_URI_PERMISSION )
805
+ }
806
+
807
+ val chooser = Intent .createChooser(shareIntent, " Share export file" )
808
+ if (chooser.resolveActivity(packageManager) != null ) {
809
+ startActivity(chooser)
810
+ } else {
811
+ Toast .makeText(this , " No apps available to share the file" , Toast .LENGTH_SHORT ).show()
812
+ }
813
+
814
+ } catch (e: Exception ) {
815
+ android.util.Log .e(" MainActivity" , " Error sharing file" , e)
816
+ Toast .makeText(this , " Error sharing file: ${e.message} " , Toast .LENGTH_SHORT ).show()
817
+ }
669
818
}
670
819
}
0 commit comments