Recycler does not show alarms [App forex alarm]

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.