Inter-Process Communication with android (phần 2)

by Cao Ngọc Hiệp
950 views

Ở phần trước mình đã giới thiệu về IPC và sử dụng IPC thông qua Messenger.

Phần 1: https://magz.techover.io/2021/12/19/inter-process-communication-with-android-phan-1/

Còn bây giờ mình sẽ sự dung IPC thông qua AIDL.

Content

  1. AIDL là gì?
  2. Tạo .aidl
  3. Demo với aidl
  4. Kết luận

1. AIDL là gì?

AIDL hay Android Interface Definition Language là một cách cho phép bạn có thể định nghĩa một cách mà cả client và server (1 ứng dụng đóng vai trò là server cho các ứng dụng khác đóng vai trò là client có thể truy cập tới) có thể giao tiếng với nhau thông qua truyền thông liên tiến trình Interprocess communication (IPC). Thông thường, trong Android một process (tiến trình) không thể trực tiếp truy cập vào bộ nhớ của một tiến trình khác. Vì vậy để có thể các tiến trình có thể giao tiếp với nhau, chúng cần phân tách các đối tượng thành dạng đối tượng nguyên thủy (primitive object) mà hệ thống có thể hiểu được.

  • Lưu ý Chỉ nên sử AIDL nếu bạn muốn các ứng dụng khác có thể kết nối tới service của ứng dụng bạn thông qua truyền thông liên tiến trinh. Còn nếu bạn chỉ đơn thuần muốn sử dụng service trong ứng dụng của mình bạn nên sử dụng Binder.

2. Tạo .aidl file

AIDL sử dụng cú pháp rất đơn giản cho phép bạn khai báo một interface với một hoặc nhiều phuơng thước có thể có một hoặc nhiều tham số và trả về một giá trị. Các tham số và giá trị trả về có thể thuộc bất kì loại nào thậm chí các interface hoặc object được tạo ra bởi các AIDL khác.

Bạn cần xây dựng một .aidl file sử dụng ngôn ngữ Java, mỗi một .aidl file cần định nghĩa duy nhất một interface.

Mặc định AIDL hộ trỡ các kiểu dữ liệu sau:

  • 8 kiểu dữ liệu nguyên thủy trong java (int, byte, short, long, float, double, boolean, char).
  • String.
  • CharSequence.
  • List, Map Tất cả các phần tử trong List, Map phải là một trong những loại dữ liệu được hỗ trợ trong danh sách này hoặc một trong các đối tượng được định nghĩa thông qua một aidl interface khác.

Khi định nghĩa aidl interface bạn cần lưu ý:

  • Phuơng thưsc có thể nhận vào 0 hoặc nhiều parameter và trả về void hoặc một giá trị cụ thể.
  • aidl chỉ hỗ trợ các phương thức không hỗ trợ các static field.

Để tạo file aidl các bạn sử dụng AS làm như sau:

Đây là một tệp .aidl ví dụ:

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

Lưu ý sau khi viết xong file aidl các bạn cần rebuild lại project để SDK tool gen ra file java để có thể sử dụng.

3. Demo với AIDL

Client

Tạo tệp .aidl:

// IIPCExample.aidl
package com.pmirkelam.ipcserver;

// Declare any non-default types here with import statements

interface IIPCExample {
    /** Request the process ID of this service */
    int getPid();

    /** Count of received connection requests from clients */
    int getConnectionCount();

    /** Set displayed value of screen */
    void setDisplayedValue(String packageName, int pid, String data);
}

fragment_aidl.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".ui.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="@string/say_something_to_server"
        android:textSize="25sp" />

    <EditText
        android:id="@+id/edt_client_data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/salut"
        android:inputType="text"
        android:textAlignment="center"
        android:textSize="35sp" />

    <Button
        android:id="@+id/btn_connect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="@string/connect"
        android:textSize="18sp" />

    <LinearLayout
        android:id="@+id/linear_layout_client_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="invisible">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/process_id_of_server"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_server_pid"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="@string/connection_count_of_server"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_server_connection_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50sp" />
    </LinearLayout>
</LinearLayout>

AIDLFragmet:

class AidlFragment : Fragment(), ServiceConnection, View.OnClickListener {

    private var _binding: FragmentAidlBinding? = null
    private val binding get() = _binding!!
    var iRemoteService: IIPCExample? = null
    private var connected = false

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentAidlBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        binding.btnConnect.setOnClickListener(this)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onClick(v: View?) {
            connected = if (connected) {
                disconnectToRemoteService()
                binding.txtServerPid.text = ""
                binding.txtServerConnectionCount.text = ""
                binding.btnConnect.text = getString(R.string.connect)
                binding.linearLayoutClientInfo.visibility = View.INVISIBLE
                false
            } else {
                connectToRemoteService()
                binding.linearLayoutClientInfo.visibility = View.VISIBLE
                binding.btnConnect.text = getString(R.string.disconnect)
                true
            }
    }
    private fun connectToRemoteService() {
        val intent = Intent("aidlexample")
        val pack = IIPCExample::class.java.`package`
        pack?.let {
            intent.setPackage(pack.name)
            activity?.applicationContext?.bindService(
                intent, this, Context.BIND_AUTO_CREATE
            )
        }
    }

    private fun disconnectToRemoteService() {
        if(connected){
            activity?.applicationContext?.unbindService(this)
        }
    }

    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        // Gets an instance of the AIDL interface named IIPCExample,
        // which we can use to call on the service
        iRemoteService = IIPCExample.Stub.asInterface(service)
        binding.txtServerPid.text = iRemoteService?.pid.toString()
        binding.txtServerConnectionCount.text = iRemoteService?.connectionCount.toString()
        iRemoteService?.setDisplayedValue(
            context?.packageName,
            Process.myPid(),
            binding.edtClientData.text.toString())
        connected = true
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        Toast.makeText(context, "IPC server has disconnected unexpectedly", Toast.LENGTH_LONG).show()
        iRemoteService = null
        connected = false
    }
}

main_activity.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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

Server

Tạo 1 tệp .aidl giống Client:

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/connection_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/no_connected_client"
        android:textSize="30sp"
        android:textStyle="bold" />

    <LinearLayout
        android:id="@+id/linear_layout_client_state"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="75dp"
            android:text="@string/package_name"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_package_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:textStyle="bold"
            tools:text="com.someone.something" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/pid"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_server_pid"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:textStyle="bold"
            tools:text="21282" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/data"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:textStyle="bold"
            tools:text="Salut!" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/ipc_method"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/txt_ipc_method"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:textStyle="bold"
            tools:text="Something" />
    </LinearLayout>

</LinearLayout>

Client.kt:

data class Client(
    var clientPackageName: String?,
    var clientProcessId: String?,
    var clientData: String?,
    var ipcMethod: String
)

object RecentClient {
    var client: Client? = null
}
  • Tạo lớp dữ liệu khách hàng.
  • Tạo một lớp RecentClient singleton nơi chúng tôi sẽ giữ máy khách được kết nối cuối cùng.

Constant.kt:

Đây là dữ liệu dùng chung.

// Bundle keys
const val PID = "pid"
const val CONNECTION_COUNT = "connection_count"
const val PACKAGE_NAME = "package_name"
const val DATA = "data"

AidlFragment.kt:

class IPCServerService : Service() {

    companion object {
        // How many connection requests have been received since the service started
        var connectionCount: Int = 0

        // Client might have sent an empty data
        const val NOT_SENT = "Not sent!"
    }

    // AIDL IPC - Binder object to pass to the client
    private val aidlBinder = object : IIPCExample.Stub() {

        override fun getPid(): Int = Process.myPid()

        override fun getConnectionCount(): Int = IPCServerService.connectionCount

        override fun setDisplayedValue(packageName: String?, pid: Int, data: String?) {
            val clientData =
                if (data == null || TextUtils.isEmpty(data)) NOT_SENT
                else data

            // Get message from client. Save recent connected client info.
            RecentClient.client = Client(
                packageName ?: NOT_SENT,
                pid.toString(),
                clientData,
                "AIDL"
            )
        }
    }

    // Pass the binder object to clients so they can communicate with this service
    override fun onBind(intent: Intent?): IBinder? {
        connectionCount++
        // Choose which binder we need to return based on the type of IPC the client makes
        return when (intent?.action) {
            "aidlexample" -> aidlBinder
            else -> null
        }
    }

    // A client has unbound from the service
    override fun onUnbind(intent: Intent?): Boolean {
        RecentClient.client = null
        return super.onUnbind(intent)
    }

}
  • Tạo đối tượng chất kết dính của chúng tôi bằng cách mở rộng từ lớp Stub. Ở đây chúng tôi điền các phương thức.
  • Trả lại đối tượng kết dính của chúng tôi cho khách hàng với ràng buộc.
  • Cập nhật đối tượng RecentClient của chúng tôi khi máy khách liên kết và hủy liên kết để giao diện người dùng được cập nhật.

Trong file Manifest:

<service
            android:name=".IPCServerService">
            <intent-filter>
                <action android:name="aidlexample" />
            </intent-filter>
        </service>

MainActivity.kt:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

    override fun onStart() {
        super.onStart()
        val client = RecentClient.client
        binding.connectionStatus.text =
            if (client == null) {
                binding.linearLayoutClientState.visibility = View.INVISIBLE
                getString(R.string.no_connected_client)
            } else {
                binding.linearLayoutClientState.visibility = View.VISIBLE
                getString(R.string.last_connected_client_info)
            }
        binding.txtPackageName.text = client?.clientPackageName
        binding.txtServerPid.text = client?.clientProcessId
        binding.txtData.text = client?.clientData
        binding.txtIpcMethod.text = client?.ipcMethod
    }
}
  • Bây giờ chúng tôi có thể cập nhật Hoạt động theo thông tin khách hàng.
  • Máy khách được kết nối cuối cùng sẽ được hiển thị. Bố cục sẽ bị ẩn nếu không có ứng dụng khách được kết nối.

Chú ý: Hầu hết các ứng dụng không nên sử dụng AIDL để tạo một bound service, bởi vì nó có thể yêu cầu khả năng đa luồng và có thể dẫn đến việc triển khai phức tạp hơn. Như vậy, AIDL không phù hợp với hầu hết các ứng dụng nên ta sẽ không thảo luận về cách sử dụng nó.

4. Kết Luận

Trên Đây là cách đơn giản nhất để thực hiện IPC bằng AIDL.

Cảm ơn mọi người đã đọc bài viết.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.