Hello everyone!
I will briefly comment on this project.
Android application developed in Kotlin, with a SQLite database to store up to a maximum of 10 alarms at the same time. The user sets an alarm to be notified when a certain currency in the FOREX market reaches the estimated value. For this, an API is developed that consults the market values in real time.
The problem happens after setting the alarm. In the recycler when there are no alarms, by default a text “There are no alarms programmed” appears after programming an alarm, the text disappears but the alarm does not appear.
I placed many logs to detect the problem but I cannot interpret it correctly.
Before the application worked correctly, but I wanted to add a DRAWER LAYOUT so that a drop-down menu appears and after this the application stopped working correctly, I removed the drawer completely, however this did not solve anything
Logs:
2024-06-10 17:27:59.330 10588-10588 Compatibil...geReporter com.example.traderforexalert D Compat change id reported: 147798919; UID 10478; state: ENABLED
2024-06-10 17:27:59.360 10588-10588 Alarms com.example.traderforexalert D Tamaño de la lista de alarmas: 3
2024-06-10 17:27:59.360 10588-10588 AlarmAdapter com.example.traderforexalert D Updating alarms with new list of size 3
2024-06-10 17:27:59.360 10588-10588 AlarmAdapter com.example.traderforexalert D Updating alarms with new list of size 3
2024-06-10 17:27:59.361 10588-10588 ActivityAlarms com.example.traderforexalert D Checking alarms...
2024-06-10 17:27:59.361 10588-10588 ActivityAlarms com.example.traderforexalert D Checking alarm: Ejemplo with fromCurrency: EUR and toCurrency: USD
2024-06-10 17:27:59.362 10588-10588 ActivityAlarms com.example.traderforexalert D Checking alarm: ejemplo with fromCurrency: EUR and toCurrency: USD
2024-06-10 17:27:59.363 10588-10588 ActivityAlarms com.example.traderforexalert D Checking alarm: Ejemplo 1 with fromCurrency: EUR and toCurrency: USD
2024-06-10 17:27:59.367 10588-10588 AlarmAdapter com.example.traderforexalert D Binding alarm Ejemplo to holder
2024-06-10 17:27:59.371 10588-10588 AlarmAdapter com.example.traderforexalert D Binding alarm ejemplo to holder
2024-06-10 17:27:59.384 10588-10588 AlarmAdapter com.example.traderforexalert D Creating new view holder for alarm
2024-06-10 17:27:59.384 10588-10588 AlarmAdapter com.example.traderforexalert D Binding alarm Ejemplo 1 to holder
2024-06-10 17:27:59.446 10588-10647 Surface com.example.traderforexalert D Surface::disconnect
2024-06-10 17:27:59.446 10588-10647 BufferQueueProducer com.example.traderforexalert D [VRI[CreateAlarmActivity]#2(BLAST Consumer)2](id:295c00000002,api:1,p:10588,c:10588) disconnect: api 1
2024-06-10 17:27:59.474 10588-10588 ImeBackDispatcher com.example.traderforexalert E Ime callback not found. Ignoring unregisterReceivedCallback. callbackId: 223794807
2024-06-10 17:27:59.767 10588-10588 ActivityAlarms com.example.traderforexalert D API Response for EURUSD: 1.07670000
2024-06-10 17:27:59.767 10588-10588 ActivityAlarms com.example.traderforexalert D Current rate: 1.0767, Target rate: 1.0901
2024-06-10 17:27:59.768 10588-10588 ActivityAlarms com.example.traderforexalert D API Response for EURUSD: 1.07670000
2024-06-10 17:27:59.768 10588-10588 ActivityAlarms com.example.traderforexalert D Current rate: 1.0767, Target rate: 1.0759
2024-06-10 17:27:59.896 10588-10588 ActivityAlarms com.example.traderforexalert D API Response for EURUSD: 1.07670000
2024-06-10 17:27:59.896 10588-10588 ActivityAlarms com.example.traderforexalert D Current rate: 1.0767, Target rate: 2.0
xml Recycler:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Main content -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<!-- Primera sección -->
<LinearLayout
android:id="@+id/first_section"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#fbba2d"
android:padding="16dp"
android:orientation="horizontal"
android:weightSum="1">
<!-- Contenido de la primera sección aquí -->
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Alarmas"
android:textColor="@android:color/black"
android:textSize="20sp"
android:gravity="center"
android:padding="16dp"/>
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageButton
android:id="@+id/button_drawer"
android:layout_width="25dp"
android:layout_height="25dp"
android:background="@android:color/transparent"
android:src="@drawable/tres_puntos"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="16dp"
android:scaleType="fitCenter"/>
</LinearLayout>
<!-- Tercera sección - ALARMAS -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:padding="10dp"
android:layout_above="@+id/button_create_alarm">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewAlarms"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"
tools:listitem="@layout/item_alarm"/>
</LinearLayout>
<!-- TextView centrado para "No hay alarmas programadas" -->
<TextView
android:id="@+id/textViewEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No hay alarmas programadas"
android:textColor="@android:color/white"
android:textSize="20sp"
android:layout_centerInParent="true"
android:gravity="center"/>
<!-- Cuarta sección -->
<Button
android:id="@+id/button_create_alarm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/round_button"
android:text="+"
android:textColor="@android:color/black"
android:textSize="25sp"
android:padding="10dp"
android:gravity="center"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="16dp"/>
</RelativeLayout>
<!-- Drawer Layout -->
<LinearLayout
android:id="@+id/drawer_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/black"
android:layout_gravity="end"
android:layout_weight="1">
<!-- Primer contenedor arriba -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="16dp">
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:src="@drawable/telegram_icon"
android:layout_marginEnd="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Telegram"
android:textColor="@android:color/white"
android:gravity="center"/>
</LinearLayout>
<!-- Segundo contenedor abajo -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="v1.0.1"
android:textColor="@android:color/white"
android:gravity="center"/>
</LinearLayout>
<!-- Tercer contenedor abajo -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="by oscarsosa94"
android:textColor="@android:color/white"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
Files KT:
ActivityAlarms.kt:
class ActivityAlarms : AppCompatActivity() {
private lateinit var dbHelper: DatabaseHelper
private lateinit var alarmAdapter: AlarmAdapter
private lateinit var recyclerViewAlarms: RecyclerView
private lateinit var textViewEmpty: TextView
private lateinit var forexApi: ForexApiService
private lateinit var drawerLayout: DrawerLayout
private lateinit var buttonDrawer: ImageButton
companion object {
private const val CHANNEL_ID = "alarm_channel"
private const val NOTIFICATION_ID = 1
private const val API_KEY = ""
private const val REQUEST_CODE_NOTIFICATION_PERMISSION = 1001
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_alarms)
dbHelper = DatabaseHelper(this)
recyclerViewAlarms = findViewById(R.id.recyclerViewAlarms)
textViewEmpty = findViewById(R.id.textViewEmpty)
val buttonCreateAlarm: Button = findViewById(R.id.button_create_alarm)
drawerLayout = findViewById(R.id.drawer_layout)
buttonDrawer = findViewById(R.id.button_drawer)
recyclerViewAlarms.layoutManager = LinearLayoutManager(this)
alarmAdapter = AlarmAdapter(emptyList()) { alarm ->
dbHelper.deleteAlarm(alarm.id)
loadAlarms()
}
recyclerViewAlarms.adapter = alarmAdapter
// Inicializar Retrofit para la API de Forex
forexApi = RetrofitClient.instance
buttonCreateAlarm.setOnClickListener {
val intent = Intent(this, CreateAlarmActivity::class.java)
startActivity(intent)
}
buttonDrawer.setOnClickListener {
if (drawerLayout.isDrawerOpen(GravityCompat.END)) {
drawerLayout.closeDrawer(GravityCompat.END)
} else {
drawerLayout.openDrawer(GravityCompat.END)
}
}
drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerClosed(drawerView: View) {
if (drawerView.id == R.id.drawer_content) {
drawerView.visibility = View.INVISIBLE
}
}
override fun onDrawerOpened(drawerView: View) {
if (drawerView.id == R.id.drawer_content) {
drawerView.visibility = View.VISIBLE
}
}
})
createNotificationChannel()
checkNotificationPermission()
}
override fun onResume() {
super.onResume()
loadAlarms()
checkAlarms()
}
private fun loadAlarms() {
val alarms = dbHelper.getAllAlarms()
Log.d("Alarms", "Tamaño de la lista de alarmas: " + alarms.size)
if (alarms.isEmpty()) {
Log.e("Alarms", "La lista de alarmas está vacía!")
} else {
alarmAdapter.updateAlarms(alarms)
}
if (alarms.isEmpty()) {
textViewEmpty.visibility = View.VISIBLE
recyclerViewAlarms.visibility = View.GONE
} else {
textViewEmpty.visibility = View.GONE
recyclerViewAlarms.visibility = View.VISIBLE
alarmAdapter.updateAlarms(alarms)
}
alarmAdapter.notifyDataSetChanged()
}
private fun checkAlarms() {
val alarms = dbHelper.getAllAlarms()
Log.d("ActivityAlarms", "Checking alarms...")
for (alarm in alarms) {
if (alarm.symbol.length == 6 && alarm.symbol.matches(Regex("^[A-Z]{6}$"))) {
val fromCurrency = alarm.symbol.substring(0, 3)
val toCurrency = alarm.symbol.substring(3, 6)
Log.d("ActivityAlarms", "Checking alarm: ${alarm.title} with fromCurrency: $fromCurrency and toCurrency: $toCurrency")
forexApi.getExchangeRate("CURRENCY_EXCHANGE_RATE", fromCurrency, toCurrency, API_KEY).enqueue(object : Callback<ExchangeRateResponse> {
override fun onResponse(call: Call<ExchangeRateResponse>, response: Response<ExchangeRateResponse>) {
if (response.isSuccessful) {
val exchangeRateResponse = response.body()
if (exchangeRateResponse != null) {
val realtimeCurrencyExchangeRate = exchangeRateResponse.realtimeCurrencyExchangeRate
val rate = realtimeCurrencyExchangeRate.exchangeRate
Log.d("ActivityAlarms", "API Response for ${alarm.symbol}: $rate")
try {
val currentRate = rate.toDouble()
Log.d("ActivityAlarms", "Current rate: $currentRate, Target rate: ${alarm.price}")
if (currentRate == alarm.price) {
notifyUser(alarm)
dbHelper.deleteAlarm(alarm.id)
loadAlarms()
}
} catch (e: NumberFormatException) {
Log.e("ActivityAlarms", "Failed to convert exchange rate to double: $rate", e)
}
} else {
Log.e("ActivityAlarms", "API Response is null for symbol: ${alarm.symbol}")
}
} else {
Log.e("ActivityAlarms", "API Response unsuccessful for symbol: ${alarm.symbol}")
}
}
override fun onFailure(call: Call<ExchangeRateResponse>, t: Throwable) {
Log.e("ActivityAlarms", "API call failed for symbol: ${alarm.symbol}", t)
}
})
} else {
Log.e("ActivityAlarms", "Symbol format is incorrect: ${alarm.symbol}")
}
}
}
private fun notifyUser(alarm: Alarm) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Alarma activada")
.setContentText("La alarma ${alarm.title} se ha activado.")
.setSmallIcon(R.drawable.ic_notification)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "Alarm Channel"
val descriptionText = "Channel for alarm notifications"
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun checkNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_NOTIFICATION_PERMISSION)
}
}
}
}
AlarmAdapter.kt:
class AlarmAdapter(
private var alarms: List<Alarm>,
private val onAlarmDeleted: (Alarm) -> Unit
) : RecyclerView.Adapter<AlarmAdapter.AlarmViewHolder>() {
// ViewHolder interno para el adaptador
inner class AlarmViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val titleTextView: TextView = view.findViewById(R.id.alarmTitle)
val symbolTextView: TextView = view.findViewById(R.id.alarmSymbol)
val priceTextView: TextView = view.findViewById(R.id.alarmPrice)
val descriptionTextView: TextView = view.findViewById(R.id.alarmDescription)
}
// Crea nuevos ViewHolders cuando el RecyclerView lo solicita
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_alarm, parent, false)
Log.d("AlarmAdapter", "Creating new view holder for alarm")
return AlarmViewHolder(view)
}
override fun onBindViewHolder(holder: AlarmViewHolder, position: Int) {
val alarm = alarms[position]
Log.d("AlarmAdapter", "Binding alarm ${alarm.title} to holder")
holder.titleTextView.text = alarm.title
holder.symbolTextView.text = alarm.symbol
holder.priceTextView.text = alarm.price.toString()
holder.descriptionTextView.text = alarm.description
// Configura un listener de clics largos para eliminar la alarma
holder.itemView.setOnLongClickListener {
onAlarmDeleted(alarm)
true
}
}
// Devuelve el tamaño de la lista de alarmas
override fun getItemCount(): Int = alarms.size
fun updateAlarms(newAlarms: List<Alarm>) {
Log.d("AlarmAdapter", "Updating alarms with new list of size ${newAlarms.size}")
val diffResult = DiffUtil.calculateDiff(AlarmDiffCallback(alarms, newAlarms))
alarms = newAlarms
diffResult.dispatchUpdatesTo(this)
}
}
class AlarmDiffCallback(private val oldList: List<Alarm>, private val newList: List<Alarm>) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
ForexApiService.kt:
interface ForexApiService {
@GET("query")
fun getExchangeRate(
@Query("function") function: String = "CURRENCY_EXCHANGE_RATE",
@Query("from_currency") fromCurrency: String,
@Query("to_currency") toCurrency: String,
@Query("apikey") apiKey: String
): Call<ExchangeRateResponse>
}
data class ExchangeRateResponse(
@SerializedName("Realtime Currency Exchange Rate") val realtimeCurrencyExchangeRate: ExchangeRate
)
data class ExchangeRate(
@SerializedName("1. From_Currency Code") val fromCurrencyCode: String,
@SerializedName("3. To_Currency Code") val toCurrencyCode: String,
@SerializedName("5. Exchange Rate") val exchangeRate: String
)
DatabaseHelper.kt:
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(SQL_CREATE_ENTRIES)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL(SQL_DELETE_ENTRIES)
onCreate(db)
}
fun insertAlarm(title: String, symbol: String, price: Double, description: String): Long {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_TITLE, title)
put(COLUMN_SYMBOL, symbol)
put(COLUMN_PRICE, price)
put(COLUMN_DESCRIPTION, description)
}
return db.insert(TABLE_NAME, null, values)
}
fun getAllAlarms(): List<Alarm> {
val db = readableDatabase
val cursor = db.query(
TABLE_NAME,
null,
null,
null,
null,
null,
null
)
val alarms = mutableListOf<Alarm>()
cursor.use {
while (it.moveToNext()) {
val id = it.getLong(it.getColumnIndexOrThrow(COLUMN_ID))
val title = it.getString(it.getColumnIndexOrThrow(COLUMN_TITLE))
val symbol = it.getString(it.getColumnIndexOrThrow(COLUMN_SYMBOL))
val price = it.getDouble(it.getColumnIndexOrThrow(COLUMN_PRICE))
val description = it.getString(it.getColumnIndexOrThrow(COLUMN_DESCRIPTION))
alarms.add(Alarm(id, title, symbol, price, description))
}
}
return alarms
}
fun getAlarmCount(): Int {
val db = readableDatabase
val cursor = db.rawQuery("SELECT COUNT(*) FROM $TABLE_NAME", null)
var count = 0
cursor.use {
if (it.moveToFirst()) {
count = it.getInt(0)
}
}
return count
}
fun deleteAlarm(alarmId: Long) {
val db = writableDatabase
val selection = "$COLUMN_ID = ?"
val selectionArgs = arrayOf(alarmId.toString())
db.delete(TABLE_NAME, selection, selectionArgs)
}
companion object {
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "Alarms.db"
const val TABLE_NAME = "alarm"
const val COLUMN_ID = "id"
const val COLUMN_TITLE = "title"
const val COLUMN_SYMBOL = "symbol"
const val COLUMN_PRICE = "price"
const val COLUMN_DESCRIPTION = "description"
private const val SQL_CREATE_ENTRIES =
"CREATE TABLE $TABLE_NAME (" +
"$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT," +
"$COLUMN_TITLE TEXT," +
"$COLUMN_SYMBOL TEXT," +
"$COLUMN_PRICE REAL," +
"$COLUMN_DESCRIPTION TEXT)"
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS $TABLE_NAME"
}
}
Thank you very much in advance if anyone can help me find the solution.