Servicios en Kotlin

Uso de servicios en Kotlin

Servicios en Android (Servicios)

Descripción general de los servicios

Cómo crear un servicio en segundo plano

IntentService

This class was deprecated in API level 30.
IntentService is subject to all the background execution limits imposed with Android 8.0 (API level 26). Consider using WorkManager instead.

Servicios en Android:

Un servicio en segundo plano:

La diferencia entre IntentService y Service

Service versus IntentService

Ejemplo: Crear un servicio para reproducir música

Código del Servicio Reproductor de Música

 

Ejemplo: Crear un servicio para descargar un fichero en segundo plano

Crear el proyecto ServicioDescarga

build.gradle:

buildFeatures {
    viewBinding = true
}

Dependencias

// implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(libs.okhttp3.okhttp)

permisos en el manifiesto:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Descarga de fichero"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.044" />

    <Switch
        android:id="@+id/switch1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="IntentService"
        app:layout_constraintBottom_toTopOf="@+id/botonIniciar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.919"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView"
        app:layout_constraintVertical_bias="1.0" />

    <Button
        android:id="@+id/botonIniciar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="36dp"
        android:text="Iniciar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.528"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <Button
        android:id="@+id/botonParar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Parar"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.534"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/botonIniciar"
        app:layout_constraintVertical_bias="0.0" />

    <TextView
        android:id="@+id/salida"
        android:layout_width="260dp"
        android:layout_height="126dp"
        android:layout_marginTop="44dp"
        android:text="resultado de la descarga"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.582"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/botonParar"
        app:layout_constraintVertical_bias="0.0" />

</androidx.constraintlayout.widget.ConstraintLayout>

añadir el objeto Memoria

object Memoria {

    private const val TAG = "Memoria"

    fun writeTextFileToMediaStoreIA(context: Context, filename: String, content: String): String {
        lateinit var outputStream: OutputStream
        lateinit var uri: Uri
        lateinit var message: String

        try {
            val resolver = context.contentResolver
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
                put(MediaStore.MediaColumns.MIME_TYPE, "text/html")
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                }
            }

            uri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)!!
            uri?.let {
                outputStream = resolver.openOutputStream(it)!!
                outputStream?.use { stream ->
                    stream.write(content.toByteArray())
                    Log.d(TAG, "Successfully wrote content to MediaStore")
                }
            } ?: run {
                Log.e(TAG, "Failed to create MediaStore entry")
                message = "Failed to create MediaStore entry"
            }
            message = "File written successfully to: $uri"
        } catch (e: IOException) {
            Log.e(TAG, "Error writing to MediaStore: ${e.message}", e)
            // Consider deleting the entry if an error occurred during writing
            // uri?.let { resolver.delete(it, null, null) }
            message =  "Error writing to MediaStore: ${e.message}"
        } finally {
            outputStream.closeSafely() // Use an extension function for safe closing (see below)
        }

        return message
    }

    // Extension function for safe closing
    private fun OutputStream.closeSafely() {
        try {
            this.close()
        } catch (e: IOException) {
            Log.w(TAG, "Error closing OutputStream: ${e.message}", e)
        }
    }

    fun writeTextFileToMediaStore(context: Context, fileName: String, data: String): String {
        val mimeType = "text/html"
        val relativePath = Environment.DIRECTORY_DOWNLOADS + "/"
        lateinit var resolver: ContentResolver
        lateinit var fileUri: Uri
        lateinit var outputStream: OutputStream
        lateinit var message: String

        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
            put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
            }
        }

        try {
            resolver = context.contentResolver
            fileUri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)!!
            outputStream = resolver.openOutputStream(fileUri)!!
            outputStream.write(data.toByteArray())
            Log.d("MyTag", "File written successfully to: $fileUri")

            message  = "File written successfully to: $fileUri"
        } catch (e: IOException) {
            Log.e("MyTag", "Error writing to file", e)
            message = "Error writing to file: ${e.message}"
        } finally {
            outputStream.close()
        }

        return message
    }

    public fun escribirExterna(contexto: Context, nombre: String, cadena: String): String {
        lateinit var tarjeta: File
        lateinit var miFichero: File
        lateinit var bw: BufferedWriter
        lateinit var mensaje: String

        try {
            // tarjeta = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            tarjeta = contexto.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
            tarjeta.mkdirs()
            miFichero = File(tarjeta!!.absolutePath, nombre)
            bw = BufferedWriter(FileWriter(miFichero))
            bw.write(cadena)
            Log.i("Información: ", miFichero.absolutePath)
            mensaje = "Fichero escrito OK\n" + miFichero.absolutePath
            Log.i("Info", mensaje)
            bw.close()
        } catch (e: IOException) {
            mensaje = e.message.toString()
            Log.e("Error de E/S", mensaje)
        }
        return mensaje
    }
}

crear el servicio en Android Studio:

y comprobar que se ha declarado el servicio en el Manifiesto:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ServicioDescarga"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".DownloadService"/>
        <service android:name=".DownloadIntentService"/>

    </application>

</manifest>

DownloadService.kt

class DownloadService : Service() {
    override fun onCreate() {
        super.onCreate()
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        lateinit var url: URL

        try {
            url = URL(MainActivity.WEB)
            descargaOkHTTP(url)
        } catch (e: MalformedURLException) {
            e.printStackTrace()
        }
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        super.onDestroy()
    }

    override fun onBind(intent: Intent): IBinder {
        // TODO: Return the communication channel to the service.
        throw UnsupportedOperationException("Not yet implemented")
    }

    private fun descargaOkHTTP(web: URL) {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(web)
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.e("Error: ", e.message.toString())
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                response.body.use { responseBody ->
                    if (!response.isSuccessful) {
                        //throw new IOException("Unexpected code " + response);
                        Log.e("Error: ", "Unexpected code $response")
                    } else {
                        // Read data on the worker thread
                        val responseData = response.body!!.string()
                        // guardar el fichero descargado en memoria externa
                        val mensaje = Memoria.escribirExterna(responseData)
                        Log.i("Descarga: ", mensaje)
                    }
                }
            }
        })
    }
}

DownloadIntentService.kt

class DownloadIntentService : IntentService("DownloadIntentService") {

    override fun onHandleIntent(intent: Intent?) {
        lateinit var url: URL

        Log.i("info", "inicio de intent service")
        if (intent != null) {
            val web = intent.extras?.getString("web")

            try {
                url = URL(web)
                Log.i("info", "inicio de okhttp")
                descargaOkHTTP(url)
            } catch (e: MalformedURLException) {
                e.printStackTrace()
                enviarRespuesta("Error en la URL: " + e.message)
            }
        }
    }

    private fun descargaOkHTTP(web: URL) {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(web)
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.e("Error: ", e.message.toString())
                enviarRespuesta("Fallo: " + e.message)
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                response.body.use { responseBody ->
                    if (!response.isSuccessful) {
                        //throw new IOException("Unexpected code " + response);
                        Log.e("Error: ", "Unexpected code $response")
                        enviarRespuesta("Error: Unexpected code $response")
                    } else {
                        // Read data on the worker thread
                        val responseData = response.body!!.string()
                        enviarRespuesta("Descarga: fichero descargado de Internet OK")
                        Log.i("info", responseData)
                        // guardar el fichero descargado en memoria externa
                        val mensaje = Memoria.writeTextFileToMediaStoreIA (this@DownloadIntentService, MainActivity.FILE_NAME, responseData)
                        // val mensaje = Memoria.writeTextFileToMediaStore (this@DownloadIntentService, MainActivity.FILE_NAME, responseData)
                        // val mensaje = Memoria.escribirExterna (this@DownloadIntentService, MainActivity.FILE_NAME, responseData)
                        enviarRespuesta(mensaje)
                        Log.i("Descarga: ", mensaje)
                    }
                }
            }
        })
    }

    private fun enviarRespuesta(mensaje: String) {
        val i = Intent()
        i.action = MainActivity.ACTION_RESP
        i.addCategory(Intent.CATEGORY_DEFAULT)
        i.putExtra("resultado", mensaje)
        sendBroadcast(i)
    }
}

MainActivity.java

class MainActivity : AppCompatActivity(), View.OnClickListener {
    lateinit var binding: ActivityMainBinding
    lateinit var intentFilter: IntentFilter
    lateinit var broadcastReceiver: BroadcastReceiver

    companion object {
        private const val REQUEST_CONNECT = 1
        const val WEB = "https://dam.org.es/ficheros/frases.html"
        const val ACTION_RESP = "RESPUESTA_DESCARGA"
        const val FILE_NAME = "frases.html"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view: View = binding.root
        setContentView(view)

        binding.botonIniciar.setOnClickListener(this)
        binding.botonParar.setOnClickListener(this)
        intentFilter = IntentFilter(ACTION_RESP)
        intentFilter.addCategory(Intent.CATEGORY_DEFAULT)
        broadcastReceiver = ReceptorOperacion()
        // registerReceiver(broadcastReceiver, intentFilter);
    }

    public override fun onResume() {
        super.onResume()
        // registrar el receptor
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(broadcastReceiver, intentFilter, RECEIVER_EXPORTED)
        } else
            registerReceiver(broadcastReceiver, intentFilter)
    }

    public override fun onPause() {
        super.onPause()
        // anular el registro del receptor
        //unregisterReceiver(broadcastReceiver)
        unregisterReceiver(broadcastReceiver);
    }

    inner class ReceptorOperacion : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            val respuesta = intent.getStringExtra("resultado")

            mostrarMensaje(respuesta!!)
            binding.salida.text = respuesta
            // mostrarMensaje(respuesta);
        }
    }

    override fun onClick(v: View) {
        lateinit var i:Intent

        binding.salida.text = ""
        if (v === binding.botonIniciar) {
            if (!binding.switch1.isChecked) {
                // uso con Service
                //mostrarMensaje("Servicio inciado")
                i = Intent(this, DownloadService::class.java)
                startService(i)
            } else {
                // uso con IntentService
                i = Intent(this, DownloadIntentService::class.java)
                i.putExtra("web", WEB)
                //mostrarMensaje("Intent Service inciado")
                startService(i)
            }
        }

        if (v === binding.botonParar) {
            if (!binding.switch1.isChecked) {
                stopService(Intent(this@MainActivity, DownloadService::class.java))
            } else {
                stopService(Intent(this@MainActivity, DownloadIntentService::class.java))
            }
        }
    }

    private fun mostrarMensaje(mensaje: String) {
        Toast.makeText(this, mensaje, Toast.LENGTH_SHORT).show()
    }

}

 

Error:

One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn't being registered exclusively for system broadcasts

Solución:

How to manage what broadcasts Broadcast Receivers receive in Kotlin

registerReceiver (broadcastReceiver, intentFilter, RECEIVER_EXPORTED);

Receptores de transmisiones

 

Código del proyecto en Github

 

Tarea online de la unidad 4 de HLC

 

Más información:

Permisos y acceso al almacenamiento externo

getExternalFilesDir

Environment.getExternalStorageDirectory() deprecated in API level 29 java

Deja una respuesta