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.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.