diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 2632a8fce4f0bba6cc3a2b252c77ef116f3b0788..da2044165cb59ffaa8c8854ee599dbe7c7fad00b 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -4,18 +4,6 @@ <value> <entry key="app"> <State> - <runningDeviceTargetSelectedWithDropDown> - <Target> - <type value="RUNNING_DEVICE_TARGET" /> - <deviceKey> - <Key> - <type value="SERIAL_NUMBER" /> - <value value="RR8R202MELK" /> - </Key> - </deviceKey> - </Target> - </runningDeviceTargetSelectedWithDropDown> - <timeTargetWasSelectedWithDropDown value="2024-04-05T01:18:14.127059300Z" /> <runningDeviceTargetsSelectedWithDialog> <Target> <type value="RUNNING_DEVICE_TARGET" /> diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6efccf1e3e7943600c92a5da175a0e05f5332baf..3342eafe518089a39c84b2f8430a3bcb17787c87 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,4 +86,8 @@ dependencies { implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + val apache_poi_version = "5.2.5" + implementation ("org.apache.poi:poi:$apache_poi_version") + implementation ("org.apache.poi:poi-ooxml:$apache_poi_version") + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 951f3a0a53b8061febf51fdb4de278736d7c9599..f4d71c3fce0a22777d6bfc1a823ce43d419f10b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Always include this permission --> @@ -28,6 +29,17 @@ android:theme="@style/Theme.BondoMan" tools:targetApi="31" tools:ignore="ForegroundServicePermission"> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="com.example.bondoman.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + <service android:name="LocationService" android:foregroundServiceType="location" android:enabled="true" android:exported="false"/> <activity android:name=".MainActivity"/> <activity diff --git a/app/src/main/java/com/example/bondoman/MainActivity.kt b/app/src/main/java/com/example/bondoman/MainActivity.kt index d1cc0b087862d14eee6b1eeddc0413ead83ff0b2..d4209da82001a39c9828a364f128937c2c39411f 100644 --- a/app/src/main/java/com/example/bondoman/MainActivity.kt +++ b/app/src/main/java/com/example/bondoman/MainActivity.kt @@ -28,6 +28,7 @@ import com.example.bondoman.room.BondomanDatabase import com.example.bondoman.services.LocationDefault import com.example.bondoman.ui.chart.ChartViewModel import com.example.bondoman.ui.nointernet.NoInternetFragment +import com.example.bondoman.ui.settings.SettingsViewModel import com.google.android.gms.location.LocationServices import retrofit2.HttpException @@ -49,12 +50,17 @@ class MainActivity : AppCompatActivity() { ChartViewModel.provideFactory(db.dao,applicationContext,this) } + private val settingsViewModel: SettingsViewModel by viewModels { + SettingsViewModel.provideFactory(db.dao, applicationContext,this) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) locationPermission() d("OKE",viewModel.state.value.toString()) - d("DONT DELETE",chartViewModel.toString()) + d("DONTDELETE",chartViewModel.toString()) + d("DONTDELETE",settingsViewModel.state.value.toString()) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) replaceFragment(CartFragment(), NoInternetFragment(), "Cart") diff --git a/app/src/main/java/com/example/bondoman/ui/settings/ExcelExtension.kt b/app/src/main/java/com/example/bondoman/ui/settings/ExcelExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..27b3f393393c55cb48e423acd600fa6ed80fc211 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/settings/ExcelExtension.kt @@ -0,0 +1,5 @@ +package com.example.bondoman.ui.settings + +enum class ExcelExtension { + XLSX, XLS +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsEvent.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..346b5b69e7d7fadb564593d29ac85e09c2d36dca --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsEvent.kt @@ -0,0 +1,5 @@ +package com.example.bondoman.ui.settings + +sealed interface SettingsEvent { + data class SetFileType(val fileType: ExcelExtension): SettingsEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt index f72fad650c5a7476a54c9b7220fed658ce7e9486..431b3ee37056b512356aae902c0a4e78099bd58b 100644 --- a/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt @@ -1,23 +1,57 @@ package com.example.bondoman.ui.settings +import android.Manifest +import android.content.Context +import android.content.Context.STORAGE_SERVICE import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle -import androidx.fragment.app.Fragment +import android.os.Looper +import android.os.storage.StorageManager +import android.util.Log.d import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.CheckBox import android.widget.Toast -import com.example.bondoman.MainActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat.checkSelfPermission +import androidx.core.content.ContextCompat.getSystemService +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.example.bondoman.LoginActivity import com.example.bondoman.R -import com.example.bondoman.databinding.FragmentChartBinding import com.example.bondoman.databinding.FragmentSettingsBinding +import com.example.bondoman.models.Transaction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.apache.poi.hssf.usermodel.HSSFWorkbook +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + class SettingsFragment : Fragment() { + private val settingsViewModel by activityViewModels<SettingsViewModel>() + private lateinit var binding: FragmentSettingsBinding private lateinit var saveButton: Button private lateinit var sendButton: Button private lateinit var logoutButton: Button + private lateinit var xlsxButton: CheckBox + private lateinit var xlsButton: CheckBox + @@ -41,38 +75,199 @@ class SettingsFragment : Fragment() { saveButton = binding.settingsSaveButton sendButton = binding.settingSendButton logoutButton = binding.settingsLogoutButton + xlsxButton = binding.SettingsXlsxCheckbox + xlsButton = binding.SettingsXlsCheckbox + + CoroutineScope(Dispatchers.Main).launch { + settingsViewModel.state.collect{ state -> + xlsxButton.isChecked = state.fileType == ExcelExtension.XLSX + xlsButton.isChecked = state.fileType == ExcelExtension.XLS + } + } saveButton.setOnClickListener { - Toast.makeText(requireContext(), "save",Toast.LENGTH_SHORT).show() + isStoragePermissionGranted() + createExcelFile(settingsViewModel.state.value.fileType,true) } sendButton.setOnClickListener { - // Create an Intent with ACTION_SEND to send an email - val emailIntent = Intent(Intent.ACTION_SEND) + createExcelFile(settingsViewModel.state.value.fileType,false) + } + + logoutButton.setOnClickListener { + logout() + } + + xlsxButton.setOnCheckedChangeListener { _, isChecked -> + if(isChecked){ + settingsViewModel._onEvent(SettingsEvent.SetFileType(ExcelExtension.XLSX)) + xlsButton.isChecked = false + } + } + xlsButton.setOnCheckedChangeListener { _, isChecked -> + if(isChecked){ + settingsViewModel._onEvent(SettingsEvent.SetFileType(ExcelExtension.XLS)) + xlsxButton.isChecked = false + } + } + + } + + private fun logout(){ + CoroutineScope(Dispatchers.IO).launch { + withContext(Dispatchers.Main){ + val sharedPreferences = requireActivity().getSharedPreferences( + "BondoMan", + Context.MODE_PRIVATE + ) + val editor = sharedPreferences.edit() + editor.remove("token") + editor.remove("email") + editor.apply() + startActivity(Intent(activity, LoginActivity::class.java)) + activity?.finish() + } + } + Toast.makeText(requireContext(), "Logged Out",Toast.LENGTH_SHORT).show() + } + + fun createExcelFile(fileType: ExcelExtension, isSaveToLocal: Boolean) { + CoroutineScope(Dispatchers.IO).launch{ + settingsViewModel.transactions.value.collect{ transaction-> + var workbook : Workbook + if(fileType == ExcelExtension.XLS){ + workbook = HSSFWorkbook() + } else { + workbook = XSSFWorkbook() + } - // Set the type to 'message/rfc822' MIME type, which is the standard MIME type for email messages - emailIntent.type = "message/rfc822" + val filledWorkbook = fillWorkbook(workbook,transaction) - // Set recipients (to whom you want to send the email) - emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf("13521106@std.stei.itb.ac.id")) + if(isSaveToLocal){ + saveWorkBook(filledWorkbook) + } else { + sendMail(filledWorkbook) + } + } + } - // Set subject of the email - emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Transaction Data") - // Set body of the email - emailIntent.putExtra(Intent.EXTRA_TEXT, "This is your data transaction") + } + private fun fillWorkbook(workbook : Workbook, transaction: List<Transaction>) : Workbook{ + val sheets = workbook.createSheet("Sheets 1") + val headerRow = sheets.createRow(0) + val header = arrayOf("Judul", "Nominal","Kategori","Lokasi","Tanggal") + for (i in 0 until header.size) { + val cell = headerRow.createCell(i) + cell.setCellValue(header[i]) + } + for (i in 1 until transaction.size+1){ + val newRow = sheets.createRow(i) + newRow.createCell(0).setCellValue(transaction[i-1].judul) + newRow.createCell(1).setCellValue(transaction[i-1].nominal.toString()) + newRow.createCell(2).setCellValue(transaction[i-1].kategori.toString()) + newRow.createCell(3).setCellValue(transaction[i-1].lokasi.address) + newRow.createCell(4).setCellValue(transaction[i-1].tanggal) + } + return workbook + } + private suspend fun saveWorkBook(workbook : Workbook) { + var filename = "BONDOMAN_EXPORT.xls" + // Create a File object like this. + val dir = File("//sdcard//Download//") - // Start the activity with the emailIntent - startActivity(Intent.createChooser(emailIntent, "Send Email")) + when (workbook) { + is XSSFWorkbook -> { + filename = "BONDOMAN_EXPORT.xlsx" + } + else -> { + // Continue + } + } - // Display a toast indicating the action - Toast.makeText(requireContext(), "Sending email...", Toast.LENGTH_SHORT).show() + val myExternalFile = File(dir,filename) + // Create an object of FileOutputStream for writing data to myFile.txt + var fos: FileOutputStream? = null + try { + // Instantiate the FileOutputStream object and pass myExternalFile in constructor + fos = FileOutputStream(myExternalFile) + // Write to the file + workbook.write(fos) + // Close the stream + fos.close() + workbook.close() + } catch (e: IOException) { + e.printStackTrace() + } + withContext(Dispatchers.Main) { + Toast.makeText(requireContext(), "File downloaded at. $myExternalFile", Toast.LENGTH_SHORT).show() } - logoutButton.setOnClickListener { - Toast.makeText(requireContext(), "logout",Toast.LENGTH_SHORT).show() + } + + private suspend fun sendMail(workbook: Workbook) { + try { + val sharedPreferences = requireActivity().getSharedPreferences( + "BondoMan", + Context.MODE_PRIVATE + ) + val email = sharedPreferences.getString("email","") + if(email == ""){ + throw IOException() + } + + // Create a temporary file to save the workbook + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + var fileName = "BONDOMAN_EXPORT_$timeStamp.xls" + if(workbook is XSSFWorkbook){ + fileName = "BONDOMAN_EXPORT_$timeStamp.xlsx" + } + val file = File(requireContext().cacheDir, fileName) + + // Write the workbook to the file + val fileOutputStream = FileOutputStream(file) + workbook.write(fileOutputStream) + fileOutputStream.close() + + val fileUri = FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", file) + + + // Create an email intent with ACTION_SEND + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, "farhan@gmail.com") + putExtra(Intent.EXTRA_SUBJECT, "Export Database Bondoman") + putExtra(Intent.EXTRA_TEXT, "This is your exported database") + putExtra(Intent.EXTRA_STREAM, fileUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + } + + startActivity(Intent.createChooser(emailIntent, "Send email...")) + } catch (e: IOException) { + e.printStackTrace() + + withContext(Dispatchers.Main) { + Toast.makeText(requireContext(), "Failed to create workbook file", Toast.LENGTH_SHORT).show() + } } + } + private fun isStoragePermissionGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + //Permission is granted + true + } else { + //Permission is revoked + ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) + false + } + } else { + // Permission is automatically granted on sdk<23 upon installation + true + } } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsState.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsState.kt new file mode 100644 index 0000000000000000000000000000000000000000..e54664fc7ecbe4b395f569ffce96ba2fe94d2d93 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsState.kt @@ -0,0 +1,5 @@ +package com.example.bondoman.ui.settings + +data class SettingsState( + val fileType : ExcelExtension = ExcelExtension.XLSX, +) diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c3c7ab5d44e702edc47f32d32143a2412fbd908 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt @@ -0,0 +1,55 @@ +package com.example.bondoman.ui.settings + +import android.content.Context +import android.os.Bundle +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.savedstate.SavedStateRegistryOwner +import com.example.bondoman.room.TransactionDao +import com.example.bondoman.services.TransactionState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class SettingsViewModel(private val dao: TransactionDao, private val context: Context): ViewModel() { + private val _state = MutableStateFlow(SettingsState()) + private val _transactions = MutableStateFlow(dao.observeAll() ) + val transactions = _transactions.asStateFlow() + val state = _state.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SettingsState()) + + fun _onEvent(event: SettingsEvent){ + when(event){ + is SettingsEvent.SetFileType -> { + _state.update { + it.copy( + fileType = event.fileType + ) + } + } + } + } + + // Define ViewModel factory in a companion object + companion object { + fun provideFactory( + dao: TransactionDao, + context: Context, + owner: SavedStateRegistryOwner, + defaultArgs: Bundle? = null, + ): AbstractSavedStateViewModelFactory = + object : AbstractSavedStateViewModelFactory(owner, defaultArgs) { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + key: String, + modelClass: Class<T>, + handle: SavedStateHandle + ): T { + return SettingsViewModel(dao,context) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index f02883c27643546ac18e2533afeaca653675ed4c..e804de014b2e569e1d6e5ad45944d13f86d113e0 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -30,18 +30,55 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/settingsSaveButton" /> + <LinearLayout + android:id="@+id/linearLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:gravity="center" + android:orientation="vertical" + app:layout_constraintTop_toBottomOf="@+id/settingSendButton" + tools:layout_editor_absoluteX="16dp"> + + <TextView + android:id="@+id/textView3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings_select_extension_title" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <CheckBox + android:id="@+id/SettingsXlsxCheckbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings_xlsx_checkbox_title" /> + + <CheckBox + android:id="@+id/SettingsXlsCheckbox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/settings_xls_checkbox_title" /> + </LinearLayout> + + </LinearLayout> + <Button android:id="@+id/settingsLogoutButton" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="28dp" + android:layout_marginTop="56dp" android:backgroundTint="@color/red.200" android:text="@string/settings_logout_button_title" android:textColor="@color/white" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/settingSendButton" /> + app:layout_constraintTop_toBottomOf="@+id/linearLayout" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49ddc8d9a0ffd999cbffedea4f08259bb9348414..8792f197e94818af8ec0c3dab7175102b22d098a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,7 @@ </string-array> <string name="chart_sort_value_title">Sort by Value</string> <string name="chart_sort_count_title">Sort by Count</string> + <string name="settings_select_extension_title">Select file extension</string> + <string name="settings_xlsx_checkbox_title">.xlsx</string> + <string name="settings_xls_checkbox_title">.xls</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..aa4e22f74d1e5c7eb8d0118522ab35264936f7c9 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,3 @@ +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path name="cache" path="/" /> +</paths> \ No newline at end of file