Comunicación Interprocesos en Android: Implementando AIDL y Distribuyéndolo en Formato JAR

La arquitectura de Android aísla los procesos de las aplicaciones, lo que impide que un proceso acceda directamente a los recursos o métodos de otro. Para superar esta limitación y facilitar la comunicación entre diferentes procesos (IPC), Android proporciona la interfaz de lenguaje de definición de Android (AIDL).

AIDL es una herramienta que permite a los desarrolladores definir interfaces programáticas que pueden ser utilizadas por clientes remotos para interactuar con un servicio. Típicamente, un servicio expone una interfaz que las aplicaciones cliente pueden invocar. Los escenarios comunes para usar AIDL incluyen:

  • Un cliente necesita enviar datos o comandos a un servicio remoto.
  • Un cliente requiere recuperar datos o resultados de operaciones de un servicio remoto.

Definiendo la Interfaz AIDL en el Servicio

Para comenzar, crearemos el archivo AIDL dentro del proyecto de la aplicación de servicio. Es crucial habilitar el soporte para AIDL en el archivo build.gradle a nivel de módullo:

android {
    ...
    buildFeatures {
        aidl true
    }
}

Ahora, en la carpeta src/main/aidl/su/paquete/, defina su interfaz AIDL. Por ejemplo, podemos crear IMyRemoteService.aidl:

package com.example.myremoteservice;

interface IMyRemoteService {
    void enviarMensaje(String texto); // Envía un mensaje al servicio
    int calcularSuma(int valorA, int valorB); // Solicita un cálculo al servicio
}

Después de definir la interfaz, es posible que deba reconstruir el proyecto (Build > Rebuild Project) para que Android Studio genere las clases stub correspondientes.

Implementando el Servicio Remoto

El siguiente paso es crear una clase de servicio en Kotlin (o Java) que extienda android.app.Service e implemente los métodos definidos en la interfaz AIDL. La implementación debe sobrescribir el método onBind() para devolver una instancia del stub AIDL.

package com.example.myremoteservice

import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.widget.Toast

// Función de extensión para mostrar Toast fácilmente
fun Context.showToastMessage(message: String) {
    Handler(Looper.getMainLooper()).post {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

class RemoteCalculatorService : Service() {

    private val serviceBinder = object : IMyRemoteService.Stub() {
        override fun enviarMensaje(texto: String) {
            applicationContext.showToastMessage("Mensaje recibido: $texto")
        }

        override fun calcularSuma(valorA: Int, valorB: Int): Int {
            return valorA + valorB
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return serviceBinder
    }
}

Finalmente, declare este servicio en el archivo AndroidManifest.xml de su aplicación de servicio. Es fundamental configurar android:exported="true" para permitir que otras aplicaciones se conecten a él:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>
        <service
            android:name=".RemoteCalculatorService"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.myremoteservice.IMyRemoteService" />
            </intent-filter>
        </service>
    </application>
</manifest>

Desarrollando el Cliente AIDL

Ahora, en un poryecto de aplicación cliente diferente, prepare la configuración para interactuar con el servicio remoto.

Configuración del Cliente

  1. **Copie el archivo AIDL**: Copie el archivo IMyRemoteService.aidl (y su estructura de paquetes) del proyecto de servicio al directorio src/main/aidl/ del proyecto cliente.
  2. **Habilite AIDL**: Añada buildFeatures { aidl true } en el build.gradle del cliente, al igual que en el servicio.
  3. **Reconstruya el Proyecto**: Esto generará las clases stub necesarias en el cliente.
  4. **Permisos de Consulta (Android 11+)**: Para Android 11 (API 30) y versiones posteriores, las aplicaciones deben declarar explícitamente los paquetes con los que desean interactuar. Añada la etiqueta <queries> en el AndroidManifest.xml del cliente, especificando el nombre del paquete del servicio remoto:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <queries>
        <package android:name="com.example.myremoteservice" />
    </queries>
    <application ...>
        ...
    </application>
</manifest>

Interfaz de Usuario del Cliente (Layout)

Cree un diseño simple, como activity_main.xml, con un botón para invocar el servicio y un TextView para mostrar los resultados:

<?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">

    <Button
        android:id="@+id/invokeServiceButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Invocar Servicio Remoto"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="16dp"
        android:text="Resultado del servicio remoto"
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@+id/invokeServiceButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Lógica del Cliente (Activity)

En la MainActivity del cliente, enlazaremos con el servicio remoto. Esto implica implementar ServiceConnection y manejar el ciclo de vida de la conexión. También es buena práctica implementar IBinder.DeathRecipient para detectar si el proceso del servicio remoto termina inesperadamente.

package com.example.aidlclient

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.example.aidlclient.databinding.ActivityMainBinding
import com.example.myremoteservice.IMyRemoteService // Importar la interfaz AIDL generada

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var remoteService: IMyRemoteService? = null
    private var messageCounter = 0

    // Receptor para manejar la muerte inesperada del proceso del servicio
    private val deathRecipient = object : IBinder.DeathRecipient {
        override fun binderDied() {
            Log.d("AIDLClient", "El proceso del servicio ha muerto.")
            remoteService?.asBinder()?.unlinkToDeath(this, 0)
            remoteService = null
            // Opcional: intentar reconectar o notificar al usuario
        }
    }

    // Callback para la conexión del servicio
    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d("AIDLClient", "Servicio conectado.")
            remoteService = IMyRemoteService.Stub.asInterface(service)
            try {
                service?.linkToDeath(deathRecipient, 0) // Monitorear la muerte del servicio
            } catch (e: RemoteException) {
                e.printStackTrace()
            }
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d("AIDLClient", "Servicio desconectado.")
            remoteService?.asBinder()?.unlinkToDeath(deathRecipient, 0)
            remoteService = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        bindRemoteService()

        binding.invokeServiceButton.setOnClickListener {
            if (remoteService != null) {
                try {
                    // Invocar métodos del servicio remoto
                    messageCounter++
                    remoteService?.enviarMensaje("Hola desde cliente, mensaje $messageCounter")
                    val sumResult = remoteService?.calcularSuma(10, 20 + messageCounter)
                    binding.resultTextView.text = "Suma remota: $sumResult"
                } catch (e: RemoteException) {
                    Log.e("AIDLClient", "Error al invocar el servicio remoto", e)
                    binding.resultTextView.text = "Error al conectar con el servicio."
                }
            } else {
                binding.resultTextView.text = "Servicio no conectado."
                Log.w("AIDLClient", "Servicio remoto no disponible.")
            }
        }
    }

    private fun bindRemoteService() {
        val intent = Intent("com.example.myremoteservice.IMyRemoteService").apply {
            // Especificar el componente para el enlace explícito
            component = ComponentName(
                "com.example.myremoteservice", // Paquete del servicio
                "com.example.myremoteservice.RemoteCalculatorService" // Nombre de la clase del servicio
            )
        }
        val bound = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        Log.d("AIDLClient", "Intent de binding enviado: $bound")
    }

    override fun onDestroy() {
        super.onDestroy()
        if (remoteService != null) {
            unbindService(serviceConnection)
            remoteService?.asBinder()?.unlinkToDeath(deathRecipient, 0)
        }
    }
}

Para probar esto, primero instale y ejecute la aplicación de servicio. Luego, instale y ejecute la aplicación cliente. Al presionar el botón del cliente, debería ver un Toast en la aplicación de servicio y el TextView del cliente actualizarse con el resultado de la suma remota.

Empaquetando la Interfaz AIDL en un Archivo JAR

En proyectos grandes o cuando se necesita compartir la interfaz AIDL con múltiples clientes sin distribuir el código fuente, es común empaquetar las clases stub generadas en un archivo JAR. Este JAR contiene solo las definiciones de interfaz y las clases auxiliares para AIDL, no la implementación del servicio.

Para crear el JAR, siga estos pasos:

  1. **Construya el proyecto de servicio**: Asegúrese de que el proyecto de servicio se haya construido al menos una vez para que se generen los archivos .class para la interfaz AIDL.
  2. **Localice los archivos .class de AIDL**: Navegue hasta el directorio de compilación del módulo de aplicación de servicio. La ruta típica es your-service-project/app/build/intermediates/javac/debug/classes/com/example/myremoteservice/ (o una ruta similar dependiendo de la versión de Gradle y el tipo de compilación). Dentro de esta carpeta, encontrará IMyRemoteService.class e IMyRemoteService$Stub.class.
  3. **Cree el JAR**: Abra una terminal en el directorio your-service-project/app/build/intermediates/javac/debug/classes/ y ejecute el siguiente comando, asegurándose de que la ruta del paquete sea correcta:
jar -cvf aidl-interface.jar com/example/myremoteservice/*.class

Esto creará un archivo aidl-interface.jar que contiene las clases necesarias para la interfaz AIDL. Puede copiar este archivo JAR a la carpeta libs del proyecto cliente y añadirlo como una dependencia. Esto elimina la necesidad de copiar el archivo .aidl directamente al cliente y simplifica la gestión de dependencias.

Etiquetas: Android AIDL IPC Kotlin JAR

Publicado el 9-21 12:11