diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aef2498083de5bcbb0e3d51cb8d7a7438f2160d2..698869905a5e41c4d8a9455e70e52d76340783a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,17 @@ android:supportsRtl="true" android:theme="@style/Theme.Exe_android" tools:targetApi="31"> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="pbd.tubes.exe_android.provider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + <activity android:name=".MainActivity" android:exported="true" @@ -34,6 +45,10 @@ android:name=".LoginActivity"> </activity> + <service android:name=".ui.login.TokenExpirationCheckService" /> + + + </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt b/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt index e7e062597fec8c38e01e1c7201cc7cffc968b947..90a8658bd6809fee9f42180adad9c682e01800a2 100644 --- a/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt +++ b/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt @@ -1,10 +1,7 @@ package pbd.tubes.exe_android -import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.net.ConnectivityManager -import android.net.NetworkCapabilities import android.os.Bundle import android.util.Log import androidx.appcompat.app.AlertDialog @@ -15,25 +12,34 @@ import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.navigation.NavigationBarView import pbd.tubes.exe_android.databinding.ActivityMainBinding +import androidx.lifecycle.Observer +import pbd.tubes.exe_android.ui.NetworkSensing.NetworkLiveData +import pbd.tubes.exe_android.ui.login.TokenExpirationCheckService +import pbd.tubes.exe_android.ui.transactions.AddTransactionFragment class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding -// private var isFabVisible : Boolean = true + private val TAG_FRAGMENT = "broadcastFragment" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (!isUserLoggedIn()) { - // Redirect to LoginActivity if not logged in startActivity(Intent(this, LoginActivity::class.java)) - finish() // Finish MainActivity so the user cannot come back without logging in + finish() return } -// val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) -// sharedPreferences.edit().clear().apply() - + + val networkStatusLiveData = NetworkLiveData(this) + networkStatusLiveData.observe(this, Observer { isConnected -> + if (!isConnected) { + showNoInternetPopup() + } + }) + + Log.d("Main", "Ini masuk main") + activateBroadcastReceiverFragment() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val navController = findNavController(R.id.nav_host_fragment_activity_main) @@ -84,37 +90,39 @@ class MainActivity : AppCompatActivity() { setupActionBarWithNavController(navController, appBarConfiguration) navigationBarView.setupWithNavController(navController) } + override fun onPause() { + super.onPause() + val serviceIntent = Intent(this, TokenExpirationCheckService::class.java) + stopService(serviceIntent) + + } + override fun onResume() { + super.onResume() + val serviceIntent = Intent(this, TokenExpirationCheckService::class.java) + startService(serviceIntent) + } + private fun isUserLoggedIn(): Boolean { val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) return sharedPreferences.contains("token") } - private fun checkNetworkConnectivity() { - val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val network = connectivityManager.activeNetwork - - val networkCapabilities = connectivityManager.getNetworkCapabilities(network) - - // Check if the network is connected and has internet capability - if (networkCapabilities != null && - (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { - // Device is connected to the internet - } else { - // Device is not connected to the internet - showNoInternetPopup() - } + private fun showNoInternetPopup() { + AlertDialog.Builder(this) + .setTitle("No Internet Connection") + .setMessage("Please check your internet connection and try again.") + .setPositiveButton("OK", null) + .show() } - private fun showNoInternetPopup() { - val builder = AlertDialog.Builder(this) - builder.setTitle("No Internet Connection") - builder.setMessage("Please check your internet connection and try again.") - builder.setPositiveButton("OK") { dialog, _ -> - dialog.dismiss() + private fun activateBroadcastReceiverFragment() { + // Check if the fragment is already added + if (supportFragmentManager.findFragmentByTag(TAG_FRAGMENT) == null) { + // If not added, add the fragment dynamically + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.add(AddTransactionFragment(), TAG_FRAGMENT) + fragmentTransaction.commit() } - val dialog = builder.create() - dialog.show() } + } \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt b/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..3bfada40dc81274885f010b0d44321593a11b125 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt @@ -0,0 +1,5 @@ +package pbd.tubes.exe_android.data + +data class TokenResponse ( + val exp: Long +) diff --git a/app/src/main/java/pbd/tubes/exe_android/data/login/ApiService.kt b/app/src/main/java/pbd/tubes/exe_android/data/login/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..69161f06827881072db8482a3cdd6e4d12fcc88e --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/login/ApiService.kt @@ -0,0 +1,19 @@ +package pbd.tubes.exe_android.data.login + +import pbd.tubes.exe_android.data.TokenResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + + +interface ApiService { + @POST("/api/auth/login") + suspend fun login(@Body credentials: LoginRequest): Response<LoginResponse> + + @POST("/api/auth/token") + suspend fun decodeToken( + @Header("Authorization") authorization: String): Response<TokenResponse> +} + + diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt b/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ea0da0a580b52a76896493a604ca8fe4bc4697e --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt @@ -0,0 +1,42 @@ +package pbd.tubes.exe_android.ui.NetworkSensing + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import androidx.lifecycle.LiveData + +class NetworkLiveData(private val context: Context) : LiveData<Boolean>() { + + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + postValue(true) + } + + override fun onLost(network: Network) { + postValue(false) + } + } + + override fun onActive() { + super.onActive() + registerNetworkCallback() + } + + override fun onInactive() { + super.onInactive() + unregisterNetworkCallback() + } + + private fun registerNetworkCallback() { + val builder = NetworkRequest.Builder() + connectivityManager.registerNetworkCallback(builder.build(), networkCallback) + } + + private fun unregisterNetworkCallback() { + connectivityManager.unregisterNetworkCallback(networkCallback) + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt b/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt index 618369d0d784218d609b7a31ecb78a02cc93b78d..c899832b1cb996acbfb35b4ebe92d5f568f23b2b 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt @@ -14,6 +14,10 @@ import pbd.tubes.exe_android.data.api.ApiService import pbd.tubes.exe_android.data.api.login.LoginRequest import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import pbd.tubes.exe_android.ui.NetworkSensing.NetworkLiveData +import pbd.tubes.exe_android.ui.login.TokenExpirationCheckService class LoginActivity : AppCompatActivity() { @@ -25,6 +29,14 @@ class LoginActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val networkStatusLiveData = NetworkLiveData(this) + networkStatusLiveData.observe(this, Observer { isConnected -> + if (!isConnected) { + showNoInternetPopup() + } + }) + setContentView(R.layout.activity_login) val retrofit = Retrofit.Builder() @@ -39,24 +51,41 @@ class LoginActivity : AppCompatActivity() { passwordEditText = findViewById(R.id.passwordEditText) loginButton.setOnClickListener { - val email = emailEditText.text.toString() - val password = passwordEditText.text.toString() +// val email = emailEditText.text.toString() +// val password = passwordEditText.text.toString() + val email = "1@std.stei.itb.ac.id" + val password = "password_1" val loginRequest = LoginRequest(email, password) login(loginRequest) } } - private fun login(loginRequest: LoginRequest) { CoroutineScope(Dispatchers.IO).launch { try { val response = apiService.login(loginRequest) if (response.isSuccessful) { val token = response.body()?.token - token?.let { - saveToken(it) - startActivity(Intent(this@LoginActivity, MainActivity::class.java)) - finish() + token?.let { tkn -> + + val responseDecode = apiService.decodeToken("Bearer $tkn") + + if (responseDecode.isSuccessful) { + val expire = responseDecode.body()?.exp + expire?.let {exp -> + startActivity(Intent(this@LoginActivity, MainActivity::class.java)) + saveTokenExpiration(exp) + saveToken(tkn) + val serviceIntent = Intent(this@LoginActivity, TokenExpirationCheckService::class.java) + startService(serviceIntent) + finish() + } + } + else { + Log.d("Failed Token", "Failed Get decoded token (error response)") + } + + } Toast.makeText(baseContext, "Log In Success!", @@ -78,6 +107,19 @@ class LoginActivity : AppCompatActivity() { sharedPreferences.edit().putString("token", token).apply() } + private fun saveTokenExpiration(expirationTime: Long) { + val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) + sharedPreferences.edit().putLong("expiration_time", expirationTime).apply() + } + + private fun showNoInternetPopup() { + AlertDialog.Builder(this) + .setTitle("No Internet Connection") + .setMessage("Please check your internet connection and try again.") + .setPositiveButton("OK", null) + .show() + } + companion object { private const val BASE_URL = "https://pbd-backend-2024.vercel.app" } diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt b/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..5711465e797474d7b1e97a4329632c7e09f056ac --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt @@ -0,0 +1,49 @@ +package pbd.tubes.exe_android.ui.login +import android.app.Notification +import android.app.Service +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.util.Log +import pbd.tubes.exe_android.LoginActivity + +class TokenExpirationCheckService : Service() { + + private val sharedPreferences by lazy { + getSharedPreferences("user_session", MODE_PRIVATE) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + checkTokenExpiration() + return START_STICKY + } + + private fun checkTokenExpiration() { + val expirationTime = sharedPreferences.getLong("expiration_time", 0) + val currentTime = System.currentTimeMillis() / 1000 + val timeDifference = expirationTime - currentTime +// Log.d("Difference", "difference is $timeDifference") + + if (currentTime > expirationTime) { + redirectToLogin() + stopSelf() + return + } + + Handler(Looper.getMainLooper()).postDelayed({ + checkTokenExpiration() + }, (timeDifference) * 1000) + } + + private fun redirectToLogin() { + val loginIntent = Intent(this, LoginActivity::class.java) + loginIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + sharedPreferences.edit().clear().apply() + startActivity(loginIntent) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt index b5814b9d70cda7b5c23a3444ae4aace0247f52eb..8fb01230815f1357eda374e56cf29b5ea1d4e06c 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt @@ -3,21 +3,28 @@ package pbd.tubes.exe_android.ui.settings import android.content.Context.MODE_PRIVATE import android.content.Intent import android.os.Bundle +import android.os.Environment import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.Toast +import androidx.core.content.FileProvider import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import pbd.tubes.exe_android.LoginActivity import pbd.tubes.exe_android.R import pbd.tubes.exe_android.databinding.FragmentSettingsBinding +import java.io.File +import kotlin.random.Random class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null + private lateinit var fragmentManager : FragmentManager // This property is only valid between onCreateView and // onDestroyView. @@ -61,7 +68,33 @@ class SettingsFragment : Fragment() { } } + val sendButton = view.findViewById<Button>(R.id.send_button) + sendButton.setOnClickListener { + val email = "13521025@std.stei.itb.ac.id" + val subject = "Transaction Details" + val text = "Please find attached the transaction details." + + sendTransactionsViaEmail(email, subject, text) + + } + + val randomize = view.findViewById<Button>(R.id.randomize_button) + randomize.setOnClickListener { + + val randomNumber = generateRandomNumber(1000, 9999) + val intent = Intent("pbd.tubes.exe_android.ACTION_RANDOMIZE_TRANSAKSI").apply { + putExtra("randomNumber", randomNumber) + } + requireContext().sendBroadcast(intent) + Log.d("Onpause setting", "Settinganjay") + findNavController().navigate(R.id.navigation_add_transaction) + + } + + val logoutButton = view.findViewById<Button>(R.id.log_out_button) + + logoutButton.setOnClickListener { logout() } @@ -71,6 +104,11 @@ class SettingsFragment : Fragment() { super.onDestroyView() _binding = null } + + override fun onPause() { + super.onPause() + + } private fun logout() { val sharedPreferences = requireContext().getSharedPreferences("user_session", MODE_PRIVATE) sharedPreferences.edit().clear().apply() @@ -79,4 +117,27 @@ class SettingsFragment : Fragment() { startActivity(intent) requireActivity().finish() } + + private fun sendTransactionsViaEmail(email: String, subject: String, text: String) { + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + val filePath = File(downloadsDir, "daftar_transaksi.xlsx") + val fileUri = FileProvider.getUriForFile( + requireContext(), + requireContext().applicationContext.packageName + ".provider", + filePath + ) + val intent = Intent(Intent.ACTION_SEND) + intent.type = "message/rfc822" + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.putExtra(Intent.EXTRA_STREAM, fileUri) + intent.setPackage("com.google.android.gm") + requireContext().startActivity(intent) + } + + private fun generateRandomNumber(min: Int, max: Int): Int { + return Random.nextInt(min, max + 1) // Generates a random number between min (inclusive) and max (inclusive) + } } \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt index 81646ae0d4c631afc56c5edcea10105bfc496f82..93bdc640d7133356449301675035d486d26d05c7 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt @@ -21,7 +21,6 @@ import java.io.File import java.io.FileOutputStream import java.util.Locale - class SettingsViewModel( private val transactionsRepository: TransactionsRepository ) : ViewModel() { diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt index 026838875e13cb5e6e0aca4e4da20457837d2bc6..0424f4706721df8f0e9727538dd26771a2bc8000 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt @@ -1,5 +1,9 @@ package pbd.tubes.exe_android.ui.transactions +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -19,10 +23,24 @@ import pbd.tubes.exe_android.models.TransactionCategory class AddTransactionFragment : Fragment() { private var _binding: FragmentAddTransactionBinding? = null - + private val intentFilter = IntentFilter("pbd.tubes.exe_android.ACTION_RANDOMIZE_TRANSAKSI") + private val receiver : BroadcastReceiver = object : BroadcastReceiver(){ + override fun onReceive(context: Context, intent: Intent){ + val randomNumber = intent.getIntExtra("randomNumber", 0) ?: 0 + random = randomNumber +// Log.d("Ini random", randomNumber.toString()) + } + } // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! + private val viewModel: AddTransactionViewModel by viewModels(factoryProducer = { AddTransactionViewModel.Factory }) + override fun onAttach(context: Context) { + super.onAttach(context) + requireContext().registerReceiver(receiver, intentFilter) + + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -30,8 +48,7 @@ class AddTransactionFragment : Fragment() { ): View { _binding = FragmentAddTransactionBinding.inflate(inflater, container, false) val root: View = binding.root - - val viewModel: AddTransactionViewModel by viewModels(factoryProducer = { AddTransactionViewModel.Factory }) +// onRandomNumberReceived(0) val transactionName: TextInputLayout = binding.transactionNameInputLayout viewModel.trName.observe(viewLifecycleOwner) { @@ -39,9 +56,15 @@ class AddTransactionFragment : Fragment() { } val transactionNominal: TextInputLayout = binding.transactionNominalInputLayout +// Log.d("NilaiRandom", "dapetnilairandom $random") viewModel.trNominal.observe(viewLifecycleOwner) { transactionNominal.editText?.setText(it) } + if (random != null){ + transactionNominal.editText?.setText(random.toString()) + } else { + transactionNominal.editText?.setText("") + } val transactionCategoryPemasukan: RadioButton = binding.radioPemasukan viewModel.trCategoryMasuk.observe(viewLifecycleOwner){ @@ -57,6 +80,7 @@ class AddTransactionFragment : Fragment() { transactionLocation.editText?.setText(it) } + fun chooseCategory() : TransactionCategory? { if (transactionCategoryPemasukan.isChecked){ return TransactionCategory.PEMASUKAN @@ -90,8 +114,18 @@ class AddTransactionFragment : Fragment() { return root } + + companion object{ + private var random: Int? = null + } + override fun onPause() { + super.onPause() + random = null + } + override fun onDestroyView() { super.onDestroyView() + requireContext().unregisterReceiver(receiver) _binding = null } } \ 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 e3e2e58134b7a044d9bcbb51143b084b4238a390..86f2a2300f62f6e866cfcb80082a1f66526f7028 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" tools:context=".ui.settings.SettingsFragment" tools:ignore="HardcodedText" + android:id="@+id/fragment_settings" > <ToggleButton @@ -79,6 +80,20 @@ app:layout_constraintStart_toStartOf="@+id/save_button" app:layout_constraintTop_toBottomOf="@+id/confirm_save_button" /> + <Button + android:id="@+id/randomize_button" + style="@style/Widget.Material3.Button" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:backgroundTint="@color/gray_200" + android:textColor="@android:color/black" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Randomize transaksi" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/save_button" + app:layout_constraintTop_toBottomOf="@+id/send_button" /> + <Button android:id="@+id/log_out_button" style="@style/Widget.Material3.Button" @@ -91,5 +106,5 @@ android:text="Keluar" app:layout_constraintEnd_toEndOf="@+id/send_button" app:layout_constraintStart_toStartOf="@+id/send_button" - app:layout_constraintTop_toBottomOf="@+id/send_button" /> + app:layout_constraintTop_toBottomOf="@+id/randomize_button" /> </androidx.constraintlayout.widget.ConstraintLayout> \ 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..791000e4925e4f5d623dbdb27894db7d2adc2057 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <external-path + name="external_files" + path="." /> +</paths> \ No newline at end of file