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
- **Copie el archivo AIDL**: Copie el archivo
IMyRemoteService.aidl(y su estructura de paquetes) del proyecto de servicio al directoriosrc/main/aidl/del proyecto cliente. - **Habilite AIDL**: Añada
buildFeatures { aidl true }en elbuild.gradledel cliente, al igual que en el servicio. - **Reconstruya el Proyecto**: Esto generará las clases stub necesarias en el cliente.
- **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 elAndroidManifest.xmldel 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:
- **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
.classpara la interfaz AIDL. - **Localice los archivos
.classde AIDL**: Navegue hasta el directorio de compilación del módulo de aplicación de servicio. La ruta típica esyour-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.classeIMyRemoteService$Stub.class. - **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.